diff --git a/.gitignore b/.gitignore index 7d4250b8..5d350aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,10 @@ fordiff/ # Local SQL workspace sql/ + +sandbox/server/backends/resources/mcp/mock_runtime/.env +sandbox/server/backends/resources/mcp/mock_runtime/certs/ +sandbox/server/backends/resources/mcp/mock_runtime/logs/ +sandbox/server/backends/resources/mcp/mock_runtime/run/ + +docs/superpowers \ No newline at end of file diff --git a/configs/sandbox-server/coding_config.json b/configs/sandbox-server/coding_config.json new file mode 100644 index 00000000..8a146405 --- /dev/null +++ b/configs/sandbox-server/coding_config.json @@ -0,0 +1,17 @@ +{ + "server": { + "url": "http://127.0.0.1:18890", + "port": 18890, + "session_ttl": 900 + }, + "resources": { + "code": { + "enabled": true, + "description": "Local coding backend for symbolic checks, Lean/Coq validation, bib processing, and plotting", + "backend_class": "sandbox.server.backends.resources.code.CodeBackend", + "config": { + "workspace_root": "${CODE_WORKSPACE_ROOT}" + } + } + } +} \ No newline at end of file diff --git a/configs/sandbox-server/mcp_config.json b/configs/sandbox-server/mcp_config.json index 18baf9c8..76cd9b78 100644 --- a/configs/sandbox-server/mcp_config.json +++ b/configs/sandbox-server/mcp_config.json @@ -10,14 +10,12 @@ "description": "Toolathlon-GYM MCP backend", "backend_class": "sandbox.server.backends.resources.mcp.toolathlon_gym.ToolathlonGymBackend", "config": { - "enabled_mcp_servers": ["filesystem", "terminal", "snowflake"], + "enabled_mcp_servers": ["excel", "filesystem", "memory", "pdf-tools", "playwright_with_chunk", "pptx", "terminal", "word", "canvas", "notion", "woocommerce"], "workspace_root": "${TOOLATHLON_WORKSPACE_ROOT:-/tmp/agentflow_mcp}", "env_overrides": { - "PGHOST": "${PGHOST:-toolathlon_pg}", - "PGPORT": "${PGPORT:-5432}", - "PGUSER": "${PGUSER:-eigent}", - "PGPASSWORD": "${PGPASSWORD:-camel}", - "PGDATABASE": "${PGDATABASE:-toolathlon_gym}" + "CANVAS_DOMAIN": "${AGENTFLOW_MCP_CANVAS_ENDPOINT:-127.0.0.1:38080}", + "BASE_URL": "${AGENTFLOW_MCP_NOTION_ENDPOINT:-http://127.0.0.1:38081}", + "WORDPRESS_SITE_URL": "${AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT:-http://127.0.0.1:38082}" } } } diff --git a/configs/synthesis/coding.json b/configs/synthesis/coding.json new file mode 100644 index 00000000..663790c9 --- /dev/null +++ b/configs/synthesis/coding.json @@ -0,0 +1,45 @@ +{ + "model_name": "deepseek/deepseek-v4-flash", + "api_key": "${OPENROUTER_API_KEY}", + "base_url": "https://openrouter.ai/api/v1", + "max_depth": 12, + "branching_factor": 2, + "depth_threshold": 2, + "min_depth": 4, + "max_selected_traj": 3, + "path_similarity_threshold": 0.72, + "number_of_seed": null, + "sandbox_server_url": "http://127.0.0.1:18890", + "sandbox_auto_start": true, + "sandbox_config_path": "configs/sandbox-server/coding_config.json", + "sandbox_timeout": 300, + "available_tools": [ + "code-*" + ], + "sampling_tips": [ + "You are exploring a local code repository, not a knowledge base.", + "Use only code tools to inspect files, search symbols, and run lightweight shell commands inside the workspace.", + "Prioritize repository structure, entrypoints, dependency files, configuration files, scripts, and tests.", + "Ground every conclusion in concrete evidence from files or command output.", + "Do not rely on outside knowledge or invent project behavior that is not supported by the repository." + ], + "synthesis_tips": [ + "We are training an assistant for repository-level coding tasks.", + "Generate realistic engineering questions that can be answered strictly from the explored repository.", + "Prefer questions about entrypoints, commands, configs, dependencies, file locations, and module relationships.", + "Answers should be short, factual, and grounded in trajectory evidence.", + "Avoid generic software trivia and avoid questions that require external documentation." + ], + "seeds_file": "seeds/coding/coding.jsonl", + "output_dir": "results/coding", + "resource_types": [ + "code" + ], + "resource_init_configs": { + "code": { + "content": { + "source_dir": "${SOURCE_DIR}" + } + } + } +} \ No newline at end of file diff --git a/configs/synthesis/mcp.json b/configs/synthesis/mcp.json new file mode 100644 index 00000000..f0f7517a --- /dev/null +++ b/configs/synthesis/mcp.json @@ -0,0 +1,44 @@ +{ + "model_name": "deepseek/deepseek-v4-flash", + "api_key": "${OPENROUTER_API_KEY}", + "base_url": "https://openrouter.ai/api/v1", + "max_depth": 12, + "branching_factor": 2, + "depth_threshold": 2, + "min_depth": 4, + "max_selected_traj": 3, + "path_similarity_threshold": 0.72, + "number_of_seed": null, + "sandbox_server_url": "http://127.0.0.1:18890", + "sandbox_auto_start": true, + "sandbox_config_path": "configs/sandbox-server/mcp_config.json", + "sandbox_timeout": 300, + "available_tools": [ + "mcp:canvas.*", + "mcp:filesystem.*", + "mcp:memory.*", + "mcp:pdf-tools.*", + "mcp:playwright_with_chunk.*", + "mcp:pptx.*", + "mcp:terminal.*", + "mcp:word.*", + "mcp:excel.*" + ], + "sampling_tips": [ + "Canvas communication for large courses is scattered across inbox conversations, announcements, discussion topics, course context, grades, submissions, and due-date signals. Teachers often need to answer repeated student questions, identify which messages require attention, and prepare targeted follow-up without manually scanning every course artifact or gradebook row.", + "They expect a context-aware communication assistant that summarizes Canvas inbox conversations and course discussion topics, drafts replies to common student questions using relevant course context, identifies students or groups who may need follow-up based on grades, late submissions, missing grading status, or upcoming deadlines, and prepares instructor-approved Canvas messages or announcements for targeted communication.", + "You have access to Canvas, a Learning Management System, which records 22 courses, 28,865 users, 32,663 enrollments, 206 assignments, 173,912 submissions and 77 quizzes. You can also use other tools provided.", + "We need mountains of training data that is diverse and challenging to train the agents help teachers meet their expectations." + ], + "synthesis_tips": [ + "Canvas communication for large courses is scattered across inbox conversations, announcements, discussion topics, course context, grades, submissions, and due-date signals. Teachers often need to answer repeated student questions, identify which messages require attention, and prepare targeted follow-up without manually scanning every course artifact or gradebook row.", + "They expect a context-aware communication assistant that summarizes Canvas inbox conversations and course discussion topics, drafts replies to common student questions using relevant course context, identifies students or groups who may need follow-up based on grades, late submissions, missing grading status, or upcoming deadlines, and prepares instructor-approved Canvas messages or announcements for targeted communication.", + "Please adopt the perspective of a teacher or a professor, and think about what kinds of questions you would ask a agent in the specific scenario, as well as what kind of answers would truly meet your needs.", + "We need mountains of training data that is diverse and challenging to train the agents help teachers meet their expectations." + ], + "seeds_file": "seeds/mcp/seeds.jsonl", + "output_dir": "results/communication", + "resource_types": [ + "mcp" + ] +} \ No newline at end of file diff --git a/examples/CodingAgent.md b/examples/CodingAgent.md new file mode 100644 index 00000000..0e50aede --- /dev/null +++ b/examples/CodingAgent.md @@ -0,0 +1,466 @@ +# 💻 CodingAgent: Local Repository Coding Backend — Data Synthesis & Debugging Guide + +This guide explains how to use AgentFlow's **Coding Backend** for **repository-grounded data synthesis** and **backend debugging**. + +Note: this guide covers **sandbox startup, synthesis runs, and debugging only**. It does **not** cover model training, deployment, inference, or evaluation. + +## 📋 Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Pipeline Overview](#pipeline-overview) +- [Step 1: Start the Sandbox Server](#step-1-start-the-sandbox-server) +- [Step 2: Run QA Synthesis](#step-2-run-qa-synthesis) +- [Step 3: Inspect Outputs](#step-3-inspect-outputs) +- [Configuration Reference](#configuration-reference) +- [FAQ / Debugging](#faq--debugging) + +--- + +## Overview + +CodingAgent is a **local repository coding agent**. For each seed, AgentFlow creates a coding workspace by copying a local repository into a sandbox directory, then lets the agent inspect or modify that workspace only through `code-*` tools. + +The Coding Backend in this repo exposes 6 core tools: + +| Tool | Description | Parameters | +|------|-------------|------------| +| `code-read` | Read a text file with line numbers | `file_path`, `offset` (optional), `limit` (optional) | +| `code-glob` | Find files by glob pattern | `pattern`, `path` (optional) | +| `code-grep` | Search file contents with a regex | `pattern`, `path` (optional), `glob` (optional) | +| `code-bash` | Run a shell command inside the current workspace | `command` | +| `code-edit` | Replace an exact string in an existing file | `file_path`, `old_string`, `new_string`, `replace_all` (optional) | +| `code-write` | Write a full file, creating parent directories if needed | `file_path`, `content` | + +Typical use cases: + +- Synthesize QA pairs grounded in a real repository +- Test whether `code-*` tools work correctly on a copied workspace +- Debug session initialization, workspace copying, and tool execution behavior + +--- + +## Prerequisites + +### 1) Install AgentFlow + +```bash +git clone https://github.com/OpenDCAI/AgentFlow +cd AgentFlow +pip install -e . +``` + +### 2) Configure LLM credentials + +The default coding synthesis config uses an OpenAI-compatible endpoint. + +```bash +export OPENROUTER_API_KEY="YOUR_KEY" +``` + +If you keep the default config values, the synthesis config will use: + +- `model_name`: `deepseek/deepseek-v4-flash` +- `base_url`: `https://openrouter.ai/api/v1` + +### 3) Configure workspace-related environment variables + +The two most important paths are: + +- `SOURCE_DIR`: the repository that will be copied into each coding workspace +- `CODE_WORKSPACE_ROOT`: the parent directory where sandbox workspaces are created + +Example: + +```bash +export SOURCE_DIR="/path/to/your/repository" +export CODE_WORKSPACE_ROOT="/tmp/agentflow_code" +``` + +### 4) Prepare a seed file + +Coding synthesis still requires a JSONL seed file. Each line must contain at least `content` and `kwargs`. + +Example: + +```jsonl +{"content":"Inspect the repository and identify the main entrypoint used to run the project.", "kwargs": {}} +{"content":"Find how this repository installs dependencies and how its test suite is executed.", "kwargs": {}} +``` + +Default seed file: + +- `seeds/coding/coding.jsonl` + +Important: for the Coding Backend, the seed describes **what to explore**, but the actual repository source comes from `resource_init_configs.code.content.source_dir` in the synthesis config, not from the seed itself. + +In the current pipeline, seeds are processed **sequentially**. + +### 5) Relevant config files + +- Sandbox config: `configs/sandbox-server/coding_config.json` +- Synthesis config: `configs/synthesis/coding.json` + +--- + +## Pipeline Overview + +The Coding Backend flow verified in this repo is: + +```text +Sandbox Server -> Code Session Initialization -> Workspace Copy -> Trajectory Sampling -> QA + Trajectory Output +``` + +For each seed, the synthesis pipeline does the following: + +1. Start or connect to the sandbox server +2. Create or reinitialize the `code` session +3. Copy `source_dir` into a workspace under `CODE_WORKSPACE_ROOT` +4. Let the agent explore that workspace using `code-*` tools +5. Save synthesized QA pairs and selected trajectories +6. On later seeds, reinitialize the `code` session for isolation; workspace removal happens later when that session is destroyed during cleanup + +Important behavior: + +- The workspace is recreated from `source_dir` when the `code` session is initialized or reinitialized +- The workspace is deleted when the corresponding `code` session is destroyed during cleanup +- Because the workspace is mutable state, **set `branching_factor=1` for Coding Backend runs** + +--- + +## Step 1: Start the Sandbox Server + +The sandbox server provides the execution environment for `code-read`, `code-glob`, `code-grep`, `code-bash`, `code-edit`, and `code-write`. + +**Command:** + +```bash +./start_sandbox_server.sh --config configs/sandbox-server/coding_config.json +``` + +> Note: `--host` and `--port` flags are ignored by `start_sandbox_server.sh`; use `server.url` and `server.port` in the config file instead. + +**Config file** `configs/sandbox-server/coding_config.json`: + +```json +{ + "server": { + "url": "http://127.0.0.1:18890", + "port": 18890, + "session_ttl": 900 + }, + "resources": { + "code": { + "enabled": true, + "description": "Local coding backend for symbolic checks, Lean/Coq validation, bib processing, and plotting", + "backend_class": "sandbox.server.backends.resources.code.CodeBackend", + "config": { + "workspace_root": "${CODE_WORKSPACE_ROOT}" + } + } + } +} +``` + +**Verification:** + +```bash +curl http://127.0.0.1:18890/health +``` + +Expected result: + +```json +{"status":"healthy"} +``` + +### Does Coding Backend need warmup? + +Usually, **no special warmup is needed**. + +Unlike heavy backends such as RAG or VM, the Coding Backend does not load a model or a global resource pool in `warmup()`. The important step is **session initialization**, where the backend creates the workspace and copies `source_dir` into it. + +So for Coding Backend debugging, focus on: + +- whether the server starts correctly +- whether the `code` session is created +- whether `source_dir` is copied into the workspace +- whether `code-*` tools can execute against that workspace + +--- + +## Step 2: Run QA Synthesis + +### Recommended config for pure Coding Backend + +In `configs/synthesis/coding.json`, keep the run strictly code-only: + +```json +{ + "available_tools": ["code-*"], + "resource_types": ["code"], + "sandbox_config_path": "configs/sandbox-server/coding_config.json" +} +``` + +Also set: + +```json +{ + "branching_factor": 1 +} +``` + +These are strong recommendations for Coding Backend debugging, not just tuning suggestions. + +Reasons: + +- `branching_factor > 1`: sibling branches in the current sampler are explored concurrently, while a single seed uses one `code` session and one mutable workspace. Different branches may read or write the same workspace and contaminate each other. + +### Run from CLI + +```bash +python3 synthesis/pipeline.py \ + --config configs/synthesis/coding.json \ + --seeds seeds/coding/coding.jsonl \ + --output-dir results/coding +``` + +If `seeds_file` and `output_dir` are already set in the config, the shorter form also works: + +```bash +python3 synthesis/pipeline.py --config configs/synthesis/coding.json +``` + +### What you should see in logs + +For a healthy pure coding run, logs usually include signals like: + +- sandbox server started or connected successfully +- `Warming up backends: ['code']` or no meaningful warmup work +- `Session created: code -> ...` +- `Available tools: ['code-read', 'code-glob', 'code-grep', 'code-bash', 'code-edit', 'code-write']` +- tool execution logs for `code-*` + +If you see tools such as `rag-search`, `web-search`, or `text2sql-execute`, your synthesis config is not pure coding. + +--- + +## Step 3: Inspect Outputs + +By default, synthesis writes two JSONL files into the output directory: + +- `synthesized_qa.jsonl` +- `trajectories.jsonl` + +Example: + +```bash +ls results/coding +``` + +Expected files: + +```text +synthesized_qa.jsonl +trajectories.jsonl +``` + +Use these files for two different debugging purposes: + +- `synthesized_qa.jsonl`: check whether the final QA pairs are repository-grounded and realistic +- `trajectories.jsonl`: check the actual tool-use path, including which files were read, which commands were run, and whether code-edit/code-write changed the workspace + +If you are debugging the backend itself, `trajectories.jsonl` is usually the more important file. + +--- + +## Configuration Reference + +### Coding synthesis config + +File: `configs/synthesis/coding.json` + +Key fields: + +| Field | Description | +|-------|-------------| +| `model_name` | LLM used for trajectory sampling and QA synthesis | +| `api_key` | OpenAI-compatible API key | +| `base_url` | OpenAI-compatible API base URL | +| `available_tools` | For pure coding, use `["code-*"]` | +| `resource_types` | For pure coding, use `["code"]` | +| `sandbox_server_url` | Sandbox server address | +| `sandbox_auto_start` | Whether the synthesis worker auto-starts the sandbox server | +| `sandbox_config_path` | Sandbox server config path used when auto-starting | +| `branching_factor` | Strongly recommend `1` for Coding Backend | +| `resource_init_configs.code.content.source_dir` | Initial repository directory copied into each workspace | +| `seeds_file` | Seed JSONL path | +| `output_dir` | Output directory for QA and trajectories | + +Recommended minimal shape: + +```json +{ + "available_tools": ["code-*"], + "resource_types": ["code"], + "branching_factor": 1, + "sandbox_server_url": "http://127.0.0.1:18890", + "sandbox_auto_start": true, + "sandbox_config_path": "configs/sandbox-server/coding_config.json", + "resource_init_configs": { + "code": { + "content": { + "source_dir": "${SOURCE_DIR}" + } + } + } +} +``` + +### Coding sandbox config + +File: `configs/sandbox-server/coding_config.json` + +Key fields: + +| Field | Description | +|-------|-------------| +| `server.url` | Sandbox listen address | +| `server.port` | Sandbox port | +| `server.session_ttl` | Session TTL in seconds | +| `resources.code.backend_class` | Backend implementation class | +| `resources.code.config.workspace_root` | Parent directory where workspaces are created | + +Recommended minimal shape: + +```json +{ + "server": { + "url": "http://127.0.0.1:18890", + "port": 18890, + "session_ttl": 900 + }, + "resources": { + "code": { + "enabled": true, + "backend_class": "sandbox.server.backends.resources.code.CodeBackend", + "config": { + "workspace_root": "${CODE_WORKSPACE_ROOT}" + } + } + } +} +``` + +--- + +## FAQ / Debugging + +### 1) Does each run create a fresh workspace? + +Yes, for each seed the pipeline creates or reinitializes the `code` session, and the Coding Backend copies `source_dir` into the workspace again. + +That means: + +- if you rerun the pipeline, the workspace starts from `source_dir` again +- if you process multiple seeds, later seeds do not continue from earlier seed edits + +### 2) Will the workspace be deleted after the run? + +Not always immediately. + +The Coding Backend deletes the workspace when `code` session cleanup runs. In the current pipeline path, intermediate seed workspaces are removed during `reinitialize()`, because the old `code` session is destroyed before the next one is created. However, the final workspace is not guaranteed to be deleted the moment the worker stops, because this pipeline currently closes the sandbox connection without always explicitly destroying the last per-seed session first. + +So if you are checking final on-disk edits, remember: + +- the workspace exists during the run +- earlier seed workspaces are typically removed when the next seed reinitializes `code` (that is, every seed use the same, brand-new workspace) +- the final workspace may remain until the session is explicitly destroyed or cleaned by session TTL expiry + +### 3) Why is `branching_factor=1` strongly recommended? + +Because the current Coding Backend uses one mutable workspace per worker/session for a given seed, while the sampler can explore sibling branches concurrently. + +With `branching_factor > 1`, one branch may: + +- edit files that another branch later reads +- overwrite files written by another branch +- make trajectories depend on sibling side effects + +That makes trajectory data hard to interpret and unsafe for backend debugging. For Coding Backend runs, set: + +```json +{ + "branching_factor": 1 +} +``` + +### 4) How do I confirm the run is pure Coding Backend? + +Check three places: + +1. `configs/synthesis/coding.json` + - `available_tools` should be `["code-*"]` + - `resource_types` should be `["code"]` +2. runtime logs + - available tools should all be `code-*` +3. sandbox config + - `configs/sandbox-server/coding_config.json` should only enable the `code` resource + +### 5) How are multiple seeds handled? + +Seeds are processed sequentially in the current pipeline, not in parallel. + +For Coding Backend runs, that means: + +- each new seed reinitializes the `code` session +- each new seed recreates the workspace from `source_dir` +- later seeds do not continue from earlier seed edits + +### 6) What if `source_dir` is wrong? + +If `source_dir` does not exist or is not a directory, `code` session initialization will fail. + +Typical checks: + +```bash +echo "$SOURCE_DIR" +ls "$SOURCE_DIR" +``` + +### 7) What if `code-glob` or `code-read` returns nothing useful? + +First verify that the workspace was actually copied from the expected repository. + +Check: + +- `SOURCE_DIR` points to the intended repository +- `resource_init_configs.code.content.source_dir` expands correctly +- `CODE_WORKSPACE_ROOT` is writable +- the run logs show successful `code` session creation + +### 8) What if `code-bash` fails? + +`code-bash` runs inside the copied workspace. If it fails, the most common causes are: + +- the command itself exits non-zero +- the expected toolchain is not installed in the runtime environment +- the command assumes a different working directory layout than the copied repository + +For debugging, start with lightweight commands such as: + +```bash +pwd +ls +find . -maxdepth 2 -type f | head +``` + +### 9) What is the difference between seeds and `source_dir`? + +They serve different purposes: + +- `seed.content`: tells the agent what to investigate +- `source_dir`: tells the backend which repository to copy into the workspace + +Changing seeds changes the exploration task. Changing `source_dir` changes the repository being explored. diff --git a/examples/MCPAgent.md b/examples/MCPAgent.md new file mode 100644 index 00000000..06381139 --- /dev/null +++ b/examples/MCPAgent.md @@ -0,0 +1,314 @@ +# MCPAgent: MCP Data Synthesis Guide (Canvas Example) + +This guide explains how to use AgentFlow's MCP backend to synthesize QA and trajectory data from the MCP server snapshots integrated into this repo. It focuses on the data synthesis flow and uses Canvas communication as a concrete example. + +AgentFlow itself only talks to the MCP endpoints configured in `configs/sandbox-server/mcp_config.json`. Those endpoints can point either to real services or to an optional local mock runtime. + +Note: this guide covers data synthesis only. It does not cover model training or deployment. + +## Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Pipeline Overview](#pipeline-overview) +- [Step 1: Prepare MCP Endpoints](#step-1-prepare-mcp-endpoints) +- [Step 2: Start the Sandbox Server](#step-2-start-the-sandbox-server) +- [Step 3: Synthesize QA Data](#step-3-synthesize-qa-data) +- [Configuration Reference](#configuration-reference) +- [FAQ](#faq) + +--- + +## Overview + +AgentFlow's MCP backend wraps a set of MCP servers behind the sandbox server. The synthesis pipeline then samples tool-use trajectories and turns them into QA data. + +For the Canvas communication example in this repo, the relevant synthesis config is: + +- `configs/synthesis/mcp.json` + +The sandbox entrypoint is: + +- `configs/sandbox-server/mcp_config.json` + +At a high level, the flow is: + +``` +MCP endpoints -> Sandbox server (MCP backend) -> Synthesis pipeline -> QA + trajectories +``` + +The important boundary is that AgentFlow only sees endpoint values. It does not need to know whether those endpoints are backed by real APIs or by a local mock runtime. + +--- + +## Prerequisites + +### 1) Install AgentFlow + +```bash +git clone https://github.com/OpenDCAI/AgentFlow +cd AgentFlow +pip install -e . +``` + +### 2) Configure LLM credentials + +The example Canvas synthesis config reads its API key from the environment: + +```bash +export OPENROUTER_API_KEY='YOUR_KEY' +``` + +If you change the model or provider, update `model_name`, `api_key`, and `base_url` in `configs/synthesis/mcp.json` accordingly. + +### 3) Prepare seed data + +The Canvas example uses a JSONL seed file. By default, the path comes from the `seeds_file` field in `configs/synthesis/mcp.json`. You can keep that default or override it with `--seeds`. + +This file uses JSONL format, one JSON object per line. Example: + +```jsonl +{"content":"Communication triage: instructors with large Canvas courses want an agent to inspect inbox conversations, course context, announcements, discussion topics, and gradebook signals to decide which student communications need attention.","kwargs":{}} +``` + +You can reuse the provided file or replace it with your own JSONL file and pass it through `--seeds`. + +### 4) MCP server snapshots are already wired in + +For the MCP integration in this repo, the copied MCP server snapshots and the main mock database snapshot from [toolathlon_gym](https://github.com/eigent-ai/toolathlon_gym) have already been wired in. You do not need to manually copy vendor resources or import database snapshots before running the example below. + +If you choose the optional mock runtime, its shim scripts still expect `TOOLATHLON_GYM_ROOT` to point at the exported MCP server snapshot root directory. + +### 5) Choose endpoint source: real services or optional mock runtime + +`configs/sandbox-server/mcp_config.json` reads these AgentFlow-side endpoint variables: + +- `AGENTFLOW_MCP_CANVAS_ENDPOINT` +- `AGENTFLOW_MCP_NOTION_ENDPOINT` +- `AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT` + +These are just endpoint values. They can point to real services, or to the optional local mock runtime described below. + +#### Option A: Point AgentFlow at real services + +Export whichever endpoints you want AgentFlow to use: + +```bash +export AGENTFLOW_MCP_CANVAS_ENDPOINT='canvas.example.edu:443' +export AGENTFLOW_MCP_NOTION_ENDPOINT='https://notion.example.internal' +export AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT='https://shop.example.internal' +``` + +#### Option B: Start the optional local mock runtime + +The mock runtime is extra infrastructure. AgentFlow does not depend on it unless you choose to point the MCP endpoints at it. + +The mock runtime in this repo uses the copied mock database setup and local HTTP shims from [toolathlon_gym](https://github.com/eigent-ai/toolathlon_gym) for: + +- `canvas` +- `notion` +- `woocommerce` + +It also requires local `docker compose`, `node`, and the exported MCP server snapshot root referenced by `TOOLATHLON_GYM_ROOT`. + +1. Copy the mock runtime env file: + +```bash +cp sandbox/server/backends/resources/mcp/mock_runtime/.env.example \ + sandbox/server/backends/resources/mcp/mock_runtime/.env +``` + +2. Generate a local Canvas TLS certificate and key: + +```bash +mkdir -p sandbox/server/backends/resources/mcp/mock_runtime/certs + +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout sandbox/server/backends/resources/mcp/mock_runtime/certs/canvas-key.pem \ + -out sandbox/server/backends/resources/mcp/mock_runtime/certs/canvas-cert.pem \ + -subj "/CN=127.0.0.1" \ + -days 365 \ + -addext "subjectAltName=IP:127.0.0.1,DNS:localhost" +``` + +3. Export the MCP server snapshot root used by the runtime scripts: + +```bash +export TOOLATHLON_GYM_ROOT=/path/to/exported_mcp_snapshot +``` + +4. If you keep the default local ports from `.env.example`, the MCP endpoint variables are optional because `mcp_config.json` already has matching fallbacks. Exporting them explicitly is still a good idea: + +```bash +export AGENTFLOW_MCP_CANVAS_ENDPOINT='127.0.0.1:38080' +export AGENTFLOW_MCP_NOTION_ENDPOINT='http://127.0.0.1:38081' +export AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT='http://127.0.0.1:38082' +``` + +5. Start the mock runtime: + +```bash +bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh +``` + +6. Check runtime health: + +```bash +bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh +``` + +For more detail on the mock runtime, see `sandbox/server/backends/resources/mcp/mock_runtime/README.md`. + +--- + +## Pipeline Overview + +The MCP synthesis pipeline in this repo is: + +``` +Endpoint preparation + -> Sandbox server with MCP backend + -> Trajectory tree sampling + -> Trajectory selection + -> QA synthesis + -> synthesized_qa.jsonl + trajectories.jsonl +``` + +For the Canvas example, the synthesis config already points at the MCP sandbox config: + +- `sandbox_config_path = configs/sandbox-server/mcp_config.json` + +It also enables sandbox auto-start: + +- `sandbox_auto_start = true` + +That means you can either start the sandbox server yourself first, or let the synthesis pipeline start it when needed. + +--- + +## Step 1: Prepare MCP Endpoints + +Before starting the sandbox, make sure the MCP endpoints referenced by `configs/sandbox-server/mcp_config.json` resolve to something reachable. + +You have two supported choices: + +1. Real service endpoints +2. The optional local mock runtime + +For the Canvas example, either choice is valid. AgentFlow only sees endpoint values and sends requests to them through the MCP backend. + +--- + +## Step 2: Start the Sandbox Server + +Start the sandbox server with the MCP backend config: + +```bash +./start_sandbox_server.sh --config configs/sandbox-server/mcp_config.json +``` + +Optional health check: + +```bash +curl http://127.0.0.1:18890/health +``` + +Expected result: + +```json +{"status":"healthy"} +``` + +If you prefer, you can skip this manual step and rely on `sandbox_auto_start=true` in the synthesis config. + +--- + +## Step 3: Synthesize QA Data + +Use the MCP synthesis config as the example entrypoint: + +- `configs/synthesis/mcp.json` + +Run: + +```bash +python -m synthesis.pipeline \ + --config configs/synthesis/mcp.json \ + --seeds /path/to/canvas_communication.jsonl \ + --output-dir results/canvas_communication +``` + +This produces: + +- `results/canvas_communication/synthesized_qa.jsonl` +- `results/canvas_communication/trajectories.jsonl` + +If you want to use the default seed file from `configs/synthesis/mcp.json`, you can omit `--seeds`. If you want a smaller smoke run, create a short JSONL file with one or a few seeds and pass that path through `--seeds`. + +--- + +## Configuration Reference + +### MCP sandbox config + +File: + +- `configs/sandbox-server/mcp_config.json` + +Key fields: + +- `resources.mcp.config.enabled_mcp_servers`: MCP servers to enable inside the backend +- `resources.mcp.config.workspace_root`: working directory exposed to MCP servers that need local workspace access +- `resources.mcp.config.env_overrides`: maps AgentFlow-side endpoint variables to the native environment variables expected by each MCP server +- `warmup.enabled` / `warmup.resources`: optional sandbox warmup configuration + +The current endpoint mapping is: + +- `AGENTFLOW_MCP_CANVAS_ENDPOINT -> CANVAS_DOMAIN` +- `AGENTFLOW_MCP_NOTION_ENDPOINT -> BASE_URL` +- `AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT -> WORDPRESS_SITE_URL` + +### Canvas synthesis config + +File: + +- `configs/synthesis/mcp.json` + +Key fields: + +- `model_name`, `api_key`, `base_url`: LLM configuration +- `sandbox_server_url`: sandbox server address +- `sandbox_auto_start`: whether the synthesis pipeline should start the sandbox automatically +- `sandbox_config_path`: sandbox config path used when auto-start is enabled +- `available_tools`: tool families exposed to the synthesis agent +- `seeds_file`: default seed JSONL path +- `max_depth`, `branching_factor`, `max_selected_traj`: trajectory sampling and selection controls + +--- + +## FAQ + +### 1) Is the mock runtime required? + +No. The mock runtime is optional. You can point AgentFlow at real MCP-backed services instead. + +### 2) Does AgentFlow know whether it is talking to a real service or a mock backend? + +No. AgentFlow only uses the endpoint values configured for the MCP backend. + +### 3) Why does the Canvas endpoint look different from Notion and WooCommerce? + +The AgentFlow-side names are unified as `AGENTFLOW_MCP_*_ENDPOINT`, but the underlying MCP servers expect different native variables. In the current config, Canvas is passed through `CANVAS_DOMAIN`, while Notion and WooCommerce expect full base URLs. + +### 4) Do I have to start the sandbox server manually? + +No. The provided Canvas synthesis config already has `sandbox_auto_start=true`. Manual startup is still useful when you want to separate sandbox bring-up from synthesis execution. + +### 5) What should I check if synthesis fails after the sandbox starts? + +Check the following first: + +- the MCP endpoints resolve to reachable services +- `OPENROUTER_API_KEY` or your chosen provider credentials are set correctly +- your seed file path is correct +- the selected model/provider combination is compatible with your synthesis workload diff --git a/sandbox/result_formatter.py b/sandbox/result_formatter.py index e1344332..9b85f41b 100644 --- a/sandbox/result_formatter.py +++ b/sandbox/result_formatter.py @@ -45,7 +45,7 @@ - rag:search: {"context": str, "query": str} - rag:batch_search: {"contexts": List[str], "count": int, "errors"?: List[Dict]} - bash: {"stdout": str, "stderr": str, "return_code": int, "cwd"?: str} -- code: {"stdout": str, "stderr": str, "return_code": int, ...} +- code: str for workspace/file tools, or {"stdout": str, "stderr": str, "return_code": int, ...} ============================================================================ @@ -57,7 +57,7 @@ - rag:stats: RAGStatsResult - RAG stats - text2sql:*: SQLResult - SQL tool results (list_databases, get_schema, execute) - bash: BashResult - bash execution result (`stdout/stderr`) -- code: CodeExecutionResult - code execution result (`stdout/stderr`) +- code: CodeExecutionResult - code tool result (string payloads or `stdout/stderr`) - browser: BrowserResult - browser operation result - vm: VMResult - VM operation result (accessibility tree only) - session:*: SessionResult - session/status API result @@ -121,7 +121,7 @@ class ToolResult(ABC): - Support custom filtering rules when needed. """ - def __init__(self, raw_data: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None): + def __init__(self, raw_data: Any, metadata: Optional[Dict[str, Any]] = None): """ Initialize a tool result object. @@ -228,13 +228,15 @@ class CodeExecutionResult(ToolResult): Code execution result. Raw data schema: - { - "stdout": str, - "stderr": str, - "return_code": int, - "execution_time_ms": float, - "memory_used_mb": float - } + - Workspace/file tools: str + - Execution-style tools: + { + "stdout": str, + "stderr": str, + "return_code": int, + "execution_time_ms": float, + "memory_used_mb": float + } """ def to_str(self, verbose: bool = False) -> str: @@ -251,6 +253,15 @@ def to_str(self, verbose: bool = False) -> str: error_msg = self.metadata.get("message", "Code execution failed") return f"[Error] {error_msg}" + if isinstance(self.raw_data, str): + return self.raw_data.rstrip("\r\n") or "[Code executed successfully with no output]" + + if not isinstance(self.raw_data, dict): + try: + return json.dumps(self.raw_data, ensure_ascii=False, separators=(",", ":")) + except Exception: + return str(self.raw_data) + stdout = self.raw_data.get("stdout", "") stderr = self.raw_data.get("stderr", "") return_code = self.raw_data.get("return_code", 0) @@ -571,6 +582,62 @@ def to_str(self, verbose: bool = False) -> str: return str(self.raw_data) +# ============================================================================ +# MCP tool result. +# ============================================================================ + +class MCPResult(ToolResult): + """MCP tool result.""" + + def to_str(self, verbose: bool = False) -> str: + del verbose + + if not self.success: + error_msg = self.metadata.get("message", "MCP tool failed") + return f"[Error] {error_msg}" + + content = self.raw_data.get("content", []) + text_blocks = [] + has_error_block = False + if isinstance(content, list): + for block in content: + if ( + isinstance(block, dict) + and block.get("type") == "text" + and isinstance(block.get("text"), str) + and block.get("text").strip() + ): + if block.get("error") is True: + has_error_block = True + text_blocks.append(block["text"].rstrip()) + + if text_blocks: + result = "\n".join(text_blocks) + else: + structured_content = self.raw_data.get("structuredContent") + if ( + isinstance(structured_content, dict) + and isinstance(structured_content.get("content"), str) + and structured_content.get("content").strip() + ): + result = structured_content["content"].rstrip() + else: + fallback_data = structured_content if structured_content else self.raw_data + try: + result = json.dumps( + fallback_data, + ensure_ascii=False, + separators=(",", ":"), + ) + except Exception: + result = str(fallback_data) + + if self.raw_data.get("isError") or has_error_block: + return f"[Error] {result}" + + return result + + # ============================================================================ # SQL tool result (text2sql). # ============================================================================ @@ -724,6 +791,7 @@ class ResultFormatter: "vm": VMResult, "doc": DocResult, "ds": DocResult, + "mcp": MCPResult, } @classmethod diff --git a/sandbox/server/backends/resources/__init__.py b/sandbox/server/backends/resources/__init__.py index 0042aa97..a981292f 100644 --- a/sandbox/server/backends/resources/__init__.py +++ b/sandbox/server/backends/resources/__init__.py @@ -62,12 +62,14 @@ from .vm import VMBackend, create_vm_backend from .rag import RAGBackend, create_rag_backend from .mcp import MCPBackend, ToolathlonGymBackend +from .code import CodeBackend __all__ = [ # Backend classes "VMBackend", "RAGBackend", "MCPBackend", + "CodeBackend", "ToolathlonGymBackend", # Convenience factories diff --git a/sandbox/server/backends/resources/code.py b/sandbox/server/backends/resources/code.py new file mode 100644 index 00000000..a4d3248a --- /dev/null +++ b/sandbox/server/backends/resources/code.py @@ -0,0 +1,342 @@ +""" +Code backend skeleton for lightweight coding workspace integration. +""" + +from __future__ import annotations + +import re +import shutil +import time +import uuid +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +from sandbox.server.backends.base import Backend, BackendConfig +from sandbox.server.backends.error_codes import ErrorCode +from sandbox.server.backends.response_builder import ( + build_error_response, + build_success_response, +) +from sandbox.server.backends.resources.code_vendor.edit_tools import EditTool, WriteTool +from sandbox.server.backends.resources.code_vendor.file_tools import ( + BashTool, + GlobTool, + GrepTool, + ReadTool, +) + + +class CodeBackend(Backend): + name = "code" + description = "Code Backend - lightweight coding workspace integration" + version = "1.0.0" + + def __init__(self, config: BackendConfig | None = None): + if config is None: + config = BackendConfig( + enabled=True, + default_config={ + "workspace_root": "/tmp/agentflow_code", + }, + description="Code backend", + ) + super().__init__(config) + self._tool_instances: dict[str, Any] | None = None + + def bind_server(self, server) -> None: + super().bind_server(server) + for tool_name in ("read", "glob", "grep", "bash", "edit", "write"): + server.register_tool( + f"code:{tool_name}", + self._make_bridge_tool(tool_name), + resource_type="code", + ) + + async def initialize(self, worker_id: str, config: dict) -> dict: + source_dir = self._resolve_source_dir(config) + workspace, staged_workspace, previous_workspace = self._prepare_workspace(worker_id) + + try: + if source_dir: + self._copy_source_dir(source_dir, staged_workspace) + + self._load_code_tools() + self._commit_prepared_workspace(workspace, staged_workspace, previous_workspace) + except Exception: + if staged_workspace.exists(): + shutil.rmtree(staged_workspace) + if previous_workspace is not None and previous_workspace.exists() and not workspace.exists(): + previous_workspace.rename(workspace) + raise + + return { + "workspace": str(workspace), + "source_dir": str(source_dir) if source_dir else "", + } + + async def cleanup(self, worker_id: str, session_info: dict) -> None: + workspace_value = ((session_info or {}).get("data") or {}).get("workspace") + if not isinstance(workspace_value, str) or not workspace_value.strip(): + return None + + try: + workspace = Path(workspace_value).resolve() + workspace_root = self._get_workspace_root().resolve() + expected_workspace = (workspace_root / self._validate_worker_id(worker_id)).resolve( + strict=False + ) + workspace.relative_to(workspace_root) + except (OSError, RuntimeError, ValueError, TypeError): + return None + + if workspace != expected_workspace: + return None + if workspace.exists() and workspace.is_dir(): + shutil.rmtree(workspace) + return None + + def _get_workspace_root(self) -> Path: + value = self.get_default_config().get("workspace_root") or "/tmp/agentflow_code" + return Path(value) + + def _prepare_workspace(self, worker_id: str) -> tuple[Path, Path, Path | None]: + safe_worker_id = self._validate_worker_id(worker_id) + workspace_root = self._get_workspace_root() + workspace_root.mkdir(parents=True, exist_ok=True) + workspace = workspace_root / safe_worker_id + staged_workspace = workspace_root / f".{safe_worker_id}.staged-{uuid.uuid4().hex}" + previous_workspace = ( + workspace_root / f".{safe_worker_id}.previous-{uuid.uuid4().hex}" + if workspace.exists() + else None + ) + staged_workspace.mkdir(parents=True, exist_ok=False) + return workspace, staged_workspace, previous_workspace + + def _commit_prepared_workspace( + self, + workspace: Path, + staged_workspace: Path, + previous_workspace: Path | None, + ) -> None: + if previous_workspace is not None: + workspace.rename(previous_workspace) + staged_workspace.rename(workspace) + if previous_workspace is not None and previous_workspace.exists(): + shutil.rmtree(previous_workspace) + + def _validate_worker_id(self, worker_id: str) -> str: + if not isinstance(worker_id, str) or not worker_id: + raise ValueError("worker_id must be a non-empty string") + if worker_id in {".", ".."}: + raise ValueError("worker_id contains unsafe path traversal") + if worker_id != Path(worker_id).name: + raise ValueError("worker_id must be a single safe path component") + if not re.fullmatch(r"[A-Za-z0-9._-]+", worker_id): + raise ValueError("worker_id contains unsupported characters") + return worker_id + + def _resolve_source_dir(self, config: dict | None) -> Path | None: + config = config or {} + value = config.get("source_dir") + if not value: + return None + source_dir = Path(value) + if not source_dir.exists(): + raise ValueError(f"source_dir does not exist: {source_dir}") + if not source_dir.is_dir(): + raise ValueError(f"source_dir is not a directory: {source_dir}") + return source_dir + + def _copy_source_dir(self, source_dir: Path, workspace: Path) -> None: + if not source_dir.exists(): + return + for child in source_dir.iterdir(): + destination = workspace / child.name + if child.is_dir(): + shutil.copytree(child, destination, dirs_exist_ok=True) + else: + shutil.copy2(child, destination) + + def _load_code_tools(self) -> dict[str, Any]: + if self._tool_instances is None: + self._tool_instances = { + "read": ReadTool(), + "glob": GlobTool(), + "grep": GrepTool(), + "bash": BashTool(), + "edit": EditTool(), + "write": WriteTool(), + } + return self._tool_instances + + def _make_bridge_tool(self, tool_name: str): + async def bridge_tool(session_info: dict, **params): + return await self._dispatch(tool_name, session_info, params) + + bridge_tool.__name__ = f"code_{tool_name}" + return bridge_tool + + async def _dispatch( + self, + tool_name: str, + session_info: dict, + params: dict[str, Any], + ) -> dict[str, Any]: + start_time = time.time() + full_name = f"{self.name}:{tool_name}" + session_id = (session_info or {}).get("session_id") + runtime_params = dict(params or {}) + trace_id = runtime_params.pop("trace_id", None) + worker_id = runtime_params.pop("worker_id", None) + runtime_params.pop("session_id", None) + + tool = self._load_code_tools().get(tool_name) + if tool is None: + return build_error_response( + code=ErrorCode.INVALID_REQUEST_FORMAT, + message=f"Unknown code tool: {tool_name}", + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + workspace_value = ((session_info or {}).get("data") or {}).get("workspace") + if not isinstance(workspace_value, str) or not workspace_value.strip(): + return build_error_response( + code=ErrorCode.BUSINESS_FAILURE, + message="Invalid session workspace: missing or empty data.workspace", + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + try: + workspace = Path(workspace_value).resolve(strict=False) + workspace_root = self._get_workspace_root().resolve() + expected_workspace = (workspace_root / self._validate_worker_id(worker_id)).resolve( + strict=False + ) + workspace.relative_to(workspace_root) + except (OSError, RuntimeError, ValueError, TypeError): + return build_error_response( + code=ErrorCode.BUSINESS_FAILURE, + message="Invalid session workspace: must resolve inside workspace_root", + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + if workspace != expected_workspace or not workspace.exists() or not workspace.is_dir(): + return build_error_response( + code=ErrorCode.BUSINESS_FAILURE, + message="Invalid session workspace: must match existing worker workspace", + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + ctx = SimpleNamespace(cwd=str(workspace)) + try: + normalized_params = self._normalize_tool_params( + tool_name=tool_name, + params=runtime_params, + workspace=workspace, + ) + except ValueError as exc: + return build_error_response( + code=ErrorCode.BUSINESS_FAILURE, + message=str(exc), + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + try: + result = await tool.call(normalized_params, ctx) + except Exception as exc: + return build_error_response( + code=ErrorCode.EXECUTION_ERROR, + message=str(exc), + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + if isinstance(result, str) and result.startswith("Error:"): + return build_error_response( + code=ErrorCode.BUSINESS_FAILURE, + message=result, + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + return build_success_response( + data=result, + tool=full_name, + execution_time_ms=(time.time() - start_time) * 1000, + resource_type=self.name, + session_id=session_id, + trace_id=trace_id, + ) + + def _normalize_tool_params( + self, + tool_name: str, + params: dict[str, Any], + workspace: Path, + ) -> dict[str, Any]: + normalized = dict(params) + workspace_path = workspace.resolve(strict=False) + + path_keys: tuple[str, ...] = () + if tool_name in {"read", "edit", "write"}: + path_keys = ("file_path",) + elif tool_name in {"glob", "grep"}: + path_keys = ("path",) + + for key in path_keys: + raw_value = normalized.get(key) + if not isinstance(raw_value, str) or not raw_value: + continue + value_path = Path(raw_value) + if value_path.is_absolute(): + resolved = value_path.resolve(strict=False) + else: + resolved = (workspace_path / value_path).resolve(strict=False) + + try: + resolved.relative_to(workspace_path) + except ValueError as exc: + raise ValueError( + f"Path parameter '{key}' must stay inside workspace" + ) from exc + + normalized[key] = str(resolved) + + if tool_name == "glob": + pattern = normalized.get("pattern") + if ( + isinstance(pattern, str) + and pattern + and re.search(r"(^|[\\/])\.\.([\\/]|$)", pattern) + ): + raise ValueError("Glob pattern must not contain parent traversal segments") + + return normalized diff --git a/sandbox/server/backends/resources/code_vendor/__init__.py b/sandbox/server/backends/resources/code_vendor/__init__.py new file mode 100644 index 00000000..1fc9cfa4 --- /dev/null +++ b/sandbox/server/backends/resources/code_vendor/__init__.py @@ -0,0 +1,11 @@ +from .edit_tools import EditTool, WriteTool +from .file_tools import BashTool, GlobTool, GrepTool, ReadTool + +__all__ = [ + "BashTool", + "EditTool", + "GlobTool", + "GrepTool", + "ReadTool", + "WriteTool", +] diff --git a/sandbox/server/backends/resources/code_vendor/edit_tools.py b/sandbox/server/backends/resources/code_vendor/edit_tools.py new file mode 100644 index 00000000..622658d7 --- /dev/null +++ b/sandbox/server/backends/resources/code_vendor/edit_tools.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from .tool import Tool + + +class EditTool(Tool): + name = "Edit" + description = ( + "Perform an exact string replacement in a file. " + "old_string must uniquely identify the target location unless replace_all=true." + ) + + @property + def input_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "file_path": {"type": "string"}, + "old_string": {"type": "string"}, + "new_string": {"type": "string"}, + "replace_all": {"type": "boolean", "default": False}, + }, + "required": ["file_path", "old_string", "new_string"], + } + + async def call(self, args: dict[str, Any], ctx: Any) -> str: + del ctx + path = Path(args["file_path"]) + old_string = args["old_string"] + new_string = args["new_string"] + replace_all = args.get("replace_all", False) + + if not path.exists(): + return f"Error: file not found: {path}" + + content = path.read_text(encoding="utf-8") + count = content.count(old_string) + if count == 0: + return f"Error: old_string not found in {path}. Read the file first to verify the exact text." + if count > 1 and not replace_all: + return ( + f"Error: old_string appears {count} times in {path}. " + "Provide more surrounding context to make it unique, or set replace_all=true." + ) + + if replace_all: + updated = content.replace(old_string, new_string) + replacements = count + else: + updated = content.replace(old_string, new_string, 1) + replacements = 1 + + path.write_text(updated, encoding="utf-8") + return f"Replaced {replacements} occurrence(s) in {path}" + + +class WriteTool(Tool): + name = "Write" + description = "Write content to a file, creating parent directories if needed." + + @property + def input_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "file_path": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["file_path", "content"], + } + + async def call(self, args: dict[str, Any], ctx: Any) -> str: + del ctx + path = Path(args["file_path"]) + content = args["content"] + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + line_count = content.count("\n") + if content and not content.endswith("\n"): + line_count += 1 + return f"Wrote {len(content)} bytes ({line_count} lines) to {path}" diff --git a/sandbox/server/backends/resources/code_vendor/file_tools.py b/sandbox/server/backends/resources/code_vendor/file_tools.py new file mode 100644 index 00000000..1a61ff2d --- /dev/null +++ b/sandbox/server/backends/resources/code_vendor/file_tools.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import asyncio +import io +import locale +import os +import signal +import subprocess +from pathlib import Path +from typing import Any + +from .tool import Tool + + +class BashTool(Tool): + name = "Bash" + description = "Execute a shell command and return stdout/stderr." + + @property + def input_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "command": {"type": "string", "description": "Shell command to run"}, + }, + "required": ["command"], + } + + async def call(self, args: dict[str, Any], ctx: Any) -> str: + proc = await asyncio.create_subprocess_shell( + args["command"], + shell=True, + cwd=ctx.cwd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + start_new_session=True, + ) + + try: + stdout_bytes, stderr_bytes = await proc.communicate() + except asyncio.CancelledError: + if proc.returncode is None: + try: + os.killpg(proc.pid, signal.SIGKILL) + except (ProcessLookupError, PermissionError): + proc.kill() + await proc.communicate() + raise + + output = _decode_text_mode_output(stdout_bytes) + stderr = _decode_text_mode_output(stderr_bytes) + if proc.returncode: + return _format_command_error("bash", proc.returncode, output, stderr) + return _format_command_output(output, stderr) + + +def _decode_text_mode_output(data: bytes | None) -> str: + if not data: + return "" + + text_stream = io.TextIOWrapper( + io.BytesIO(data), + encoding=locale.getpreferredencoding(False), + newline=None, + ) + try: + return text_stream.read() + finally: + text_stream.detach() + + +def _format_command_output(stdout: str, stderr: str) -> str: + output = stdout + if stderr: + output += f"\n[stderr]:\n{stderr}" if output else f"[stderr]:\n{stderr}" + return output.strip() or "(no output)" + + +def _format_command_error(tool_name: str, returncode: int, stdout: str, stderr: str) -> str: + if returncode < 0: + status = f"signal {-returncode}" + else: + status = f"exit status {returncode}" + + summary = f"Error: {tool_name} command failed with {status}" + details = _format_command_output(stdout, stderr) + if details == "(no output)": + return summary + return f"{summary}\n{details}" + + +class ReadTool(Tool): + name = "Read" + description = "Read a file and return its contents with line numbers." + + @property + def input_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "file_path": {"type": "string"}, + "offset": {"type": "integer", "description": "Start line (1-indexed)"}, + "limit": {"type": "integer", "description": "Maximum lines to return"}, + }, + "required": ["file_path"], + } + + async def call(self, args: dict[str, Any], ctx: Any) -> str: + del ctx + path = Path(args["file_path"]) + if not path.exists(): + return f"Error: file not found: {path}" + + lines = path.read_text(encoding="utf-8").splitlines() + offset = max(0, args.get("offset", 1) - 1) + limit = args.get("limit", 2000) + selected = lines[offset : offset + limit] + return "\n".join( + f"{line_number:4}→{line}" + for line_number, line in enumerate(selected, start=offset + 1) + ) + + def is_read_only(self, args: dict[str, Any]) -> bool: + del args + return True + + +class GlobTool(Tool): + name = "Glob" + description = "Find files matching a glob pattern." + + @property + def input_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "pattern": {"type": "string", "description": "Glob pattern"}, + "path": {"type": "string", "description": "Directory to search from"}, + }, + "required": ["pattern"], + } + + async def call(self, args: dict[str, Any], ctx: Any) -> str: + base = Path(args.get("path", ctx.cwd)) + pattern = args["pattern"] + matches = sorted(base.glob(pattern)) + return "\n".join(str(match) for match in matches) or "(no matches)" + + def is_read_only(self, args: dict[str, Any]) -> bool: + del args + return True + + +class GrepTool(Tool): + name = "Grep" + description = "Search file contents with a regex pattern." + + @property + def input_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "pattern": {"type": "string", "description": "Regex pattern"}, + "path": {"type": "string", "description": "Directory to search"}, + "glob": {"type": "string", "description": "Optional file glob filter"}, + }, + "required": ["pattern"], + } + + async def call(self, args: dict[str, Any], ctx: Any) -> str: + base = Path(args.get("path", ctx.cwd)) + cmd = ["grep", "-r", "-n"] + if "glob" in args: + cmd += ["--include", args["glob"]] + cmd += ["--", args["pattern"], str(base)] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode == 0: + return result.stdout or "(no matches)" + if result.returncode == 1: + return "(no matches)" + return _format_command_error("grep", result.returncode, result.stdout, result.stderr) + + def is_read_only(self, args: dict[str, Any]) -> bool: + del args + return True diff --git a/sandbox/server/backends/resources/code_vendor/tool.py b/sandbox/server/backends/resources/code_vendor/tool.py new file mode 100644 index 00000000..bef70845 --- /dev/null +++ b/sandbox/server/backends/resources/code_vendor/tool.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + + +class Tool(ABC): + name: str + description: str + + @property + @abstractmethod + def input_schema(self) -> dict[str, Any]: + raise NotImplementedError + + @abstractmethod + async def call(self, args: dict[str, Any], ctx: Any) -> str: + raise NotImplementedError + + def is_read_only(self, args: dict[str, Any]) -> bool: + del args + return False + + def to_api_format(self) -> dict[str, Any]: + return { + "name": self.name, + "description": self.description, + "input_schema": self.input_schema, + } diff --git a/sandbox/server/backends/resources/mcp/client.py b/sandbox/server/backends/resources/mcp/client.py index da83acc8..17b17aa6 100644 --- a/sandbox/server/backends/resources/mcp/client.py +++ b/sandbox/server/backends/resources/mcp/client.py @@ -14,6 +14,7 @@ logger = logging.getLogger("MCPStdioClient") +_MCP_STDIO_STREAM_LIMIT = 8 * 1024 * 1024 # Covers observed Canvas conversations payloads with room to spare. _PLACEHOLDER_PATTERN = re.compile(r"\$\{([^}]+)\}") _SUPPORTED_PLACEHOLDERS = {"local_servers_paths", "agent_workspace", "task_dir"} _BUNDLED_CONFIG_DIR = Path(__file__).parent / "configs" @@ -186,6 +187,7 @@ async def start(self) -> None: stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + limit=_MCP_STDIO_STREAM_LIMIT, env=self._config.env, cwd=self._config.cwd, ) diff --git a/sandbox/server/backends/resources/mcp/configs/canvas.yaml b/sandbox/server/backends/resources/mcp/configs/canvas.yaml index f3c2ffcd..7162d95d 100644 --- a/sandbox/server/backends/resources/mcp/configs/canvas.yaml +++ b/sandbox/server/backends/resources/mcp/configs/canvas.yaml @@ -7,17 +7,12 @@ name: canvas params: command: node args: - - "${local_servers_paths}/mcp-canvas-lms/environment/build/index.js" + - "${local_servers_paths}/mcp-canvas-lms/build/index.js" env: CANVAS_API_TOKEN: "${token.canvas_api_token}" CANVAS_STUDENT_EMAIL: "${token.canvas_student_email}" CANVAS_DOMAIN: "localhost:8080" NODE_TLS_REJECT_UNAUTHORIZED: "0" - PG_HOST: "${config.pg_host}" - PG_PORT: "5434" - PG_DATABASE: "${config.pg_database}" - PG_USER: "eigent" - PG_PASSWORD: "camel" cwd: "${agent_workspace}" client_session_timeout_seconds: 60 cache_tools_list: true diff --git a/sandbox/server/backends/resources/mcp/configs/excel.yaml b/sandbox/server/backends/resources/mcp/configs/excel.yaml index b0285a45..dee1fb3c 100644 --- a/sandbox/server/backends/resources/mcp/configs/excel.yaml +++ b/sandbox/server/backends/resources/mcp/configs/excel.yaml @@ -7,7 +7,7 @@ params: command: uv args: - "--directory" - - "${local_servers_paths}/excel-mcp-server/environment" + - "${local_servers_paths}/excel-mcp-server" - "run" - "excel-mcp-server" - "stdio" diff --git a/sandbox/server/backends/resources/mcp/configs/filesystem.yaml b/sandbox/server/backends/resources/mcp/configs/filesystem.yaml index 077ffb97..fee30d19 100644 --- a/sandbox/server/backends/resources/mcp/configs/filesystem.yaml +++ b/sandbox/server/backends/resources/mcp/configs/filesystem.yaml @@ -6,7 +6,7 @@ name: filesystem params: command: node args: - - "${local_servers_paths}/filesystem/environment/dist/index.js" + - "${local_servers_paths}/filesystem/dist/index.js" - "${agent_workspace}" cwd: "${agent_workspace}" client_session_timeout_seconds: 900 diff --git a/sandbox/server/backends/resources/mcp/configs/notion.yaml b/sandbox/server/backends/resources/mcp/configs/notion.yaml index 570f3bd8..ee20d2f5 100644 --- a/sandbox/server/backends/resources/mcp/configs/notion.yaml +++ b/sandbox/server/backends/resources/mcp/configs/notion.yaml @@ -7,14 +7,9 @@ name: notion params: command: node args: - - "${local_servers_paths}/notion-mcp-server/environment/bin/cli-dev.mjs" + - "${local_servers_paths}/notion-mcp-server/bin/cli.mjs" env: OPENAPI_MCP_HEADERS: "{\"Authorization\": \"Bearer placeholder\", \"Notion-Version\": \"2022-06-28\" }" - PG_HOST: "${config.pg_host}" - PG_PORT: "5434" - PG_DATABASE: "${config.pg_database}" - PG_USER: "eigent" - PG_PASSWORD: "camel" HTTP_PROXY: "" HTTPS_PROXY: "" http_proxy: "" diff --git a/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml b/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml index 6446da85..9289979f 100644 --- a/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml +++ b/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml @@ -7,7 +7,7 @@ params: command: uv args: - "--directory" - - "${local_servers_paths}/pdf-tools-mcp/environment" + - "${local_servers_paths}/pdf-tools-mcp" - "run" - "pdf-tools-mcp" - "--workspace_path" diff --git a/sandbox/server/backends/resources/mcp/configs/pptx.yaml b/sandbox/server/backends/resources/mcp/configs/pptx.yaml index 4e5753c1..79d89e77 100644 --- a/sandbox/server/backends/resources/mcp/configs/pptx.yaml +++ b/sandbox/server/backends/resources/mcp/configs/pptx.yaml @@ -7,7 +7,7 @@ params: command: uv args: - "--directory" - - "${local_servers_paths}/Office-PowerPoint-MCP-Server/environment" + - "${local_servers_paths}/Office-PowerPoint-MCP-Server" - "run" - "ppt_mcp_server" cwd: "${agent_workspace}" diff --git a/sandbox/server/backends/resources/mcp/configs/terminal.yaml b/sandbox/server/backends/resources/mcp/configs/terminal.yaml index 1b897230..4876af42 100644 --- a/sandbox/server/backends/resources/mcp/configs/terminal.yaml +++ b/sandbox/server/backends/resources/mcp/configs/terminal.yaml @@ -8,7 +8,7 @@ params: command: uv args: - "--directory" - - "${local_servers_paths}/cli-mcp-server/environment" + - "${local_servers_paths}/cli-mcp-server" - "run" - "cli-mcp-server" env: diff --git a/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml b/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml index eacd62c4..035dcb2a 100644 --- a/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml +++ b/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml @@ -6,16 +6,11 @@ name: woocommerce params: command: node args: - - "${local_servers_paths}/woocommerce-mcp/environment/dist/index.js" + - "${local_servers_paths}/woocommerce-mcp/dist/index.js" env: WORDPRESS_SITE_URL: "http://localhost" WOOCOMMERCE_CONSUMER_KEY: "placeholder" WOOCOMMERCE_CONSUMER_SECRET: "placeholder" - PG_HOST: "${config.pg_host}" - PG_PORT: "5434" - PG_DATABASE: "${config.pg_database}" - PG_USER: "eigent" - PG_PASSWORD: "camel" cwd: "${agent_workspace}" client_session_timeout_seconds: 60 cache_tools_list: true diff --git a/sandbox/server/backends/resources/mcp/configs/word.yaml b/sandbox/server/backends/resources/mcp/configs/word.yaml index a15a1e5b..9f162511 100644 --- a/sandbox/server/backends/resources/mcp/configs/word.yaml +++ b/sandbox/server/backends/resources/mcp/configs/word.yaml @@ -7,7 +7,7 @@ params: command: uv args: - "--directory" - - "${local_servers_paths}/Office-Word-MCP-Server/environment" + - "${local_servers_paths}/Office-Word-MCP-Server" - "run" - "word_mcp_server" cwd: "${agent_workspace}" diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/.env.example b/sandbox/server/backends/resources/mcp/mock_runtime/.env.example new file mode 100644 index 00000000..309a80f0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/.env.example @@ -0,0 +1,15 @@ +POSTGRES_HOST_PORT=5432 +POSTGRES_DB=toolathlon_gym +POSTGRES_USER=eigent +POSTGRES_PASSWORD=camel + +CANVAS_SHIM_HOST=127.0.0.1 +CANVAS_SHIM_PORT=38080 +CANVAS_TLS_CERT_PATH=./certs/canvas-cert.pem +CANVAS_TLS_KEY_PATH=./certs/canvas-key.pem + +NOTION_SHIM_HOST=127.0.0.1 +NOTION_SHIM_PORT=38081 + +WOOCOMMERCE_SHIM_HOST=127.0.0.1 +WOOCOMMERCE_SHIM_PORT=38082 diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/README.md b/sandbox/server/backends/resources/mcp/mock_runtime/README.md new file mode 100644 index 00000000..93961193 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/README.md @@ -0,0 +1,103 @@ +# MCP Mock Runtime + +This directory contains an optional standalone mock runtime for AgentFlow's MCP backend. + +AgentFlow is transparent to mock vs. real services. It only depends on the endpoint values wired into its own MCP config: + +- `AGENTFLOW_MCP_CANVAS_ENDPOINT` +- `AGENTFLOW_MCP_NOTION_ENDPOINT` +- `AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT` + +The current mock-runtime scope only covers these three HTTP-backed MCP integrations: + +- `canvas` +- `notion` +- `woocommerce` + +## What Starts Here + +`scripts/start_mock_runtime.sh` brings up: + +- the local `postgres` container from `docker-compose.yml` +- the three shim servers under `shims/canvas`, `shims/notion`, and `shims/woocommerce` + +This runtime is extra infrastructure. AgentFlow itself still just starts the MCP servers listed in its default config and uses the endpoint overrides above for the three HTTP-backed services. + +At the current defaults, that means the following self-contained MCP servers can start alongside the mock-backed HTTP trio: + +- `excel` +- `filesystem` +- `howtocook` +- `memory` +- `pdf-tools` +- `playwright_with_chunk` +- `pptx` +- `terminal` +- `word` + +## Bring-Up + +1. Copy the example env file: + +```bash +cp sandbox/server/backends/resources/mcp/mock_runtime/.env.example \ + sandbox/server/backends/resources/mcp/mock_runtime/.env +``` + +2. Update the copied `.env` before startup: + +- `CANVAS_TLS_CERT_PATH` and `CANVAS_TLS_KEY_PATH` in `.env.example` are placeholder paths. +- Point both values at real certificate files before running the shim. +- If your team already keeps test certificates under `TOOLATHLON_GYM_ROOT`, you can repoint these paths there. + +3. Export the runtime root and the AgentFlow-visible MCP endpoints: + +```bash +export TOOLATHLON_GYM_ROOT=/path/to/exported_mcp_snapshot +export AGENTFLOW_MCP_CANVAS_ENDPOINT=127.0.0.1:38080 +export AGENTFLOW_MCP_NOTION_ENDPOINT=http://127.0.0.1:38081 +export AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT=http://127.0.0.1:38082 +``` + +4. Start the mock runtime: + +```bash +bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh +``` + +Current default shim ports from `.env.example` are: + +- `canvas`: `https://127.0.0.1:38080` +- `notion`: `http://127.0.0.1:38081` +- `woocommerce`: `http://127.0.0.1:38082` + +Those defaults match AgentFlow's current MCP env overrides: + +- `CANVAS_DOMAIN=${AGENTFLOW_MCP_CANVAS_ENDPOINT:-127.0.0.1:38080}` +- `BASE_URL=${AGENTFLOW_MCP_NOTION_ENDPOINT:-http://127.0.0.1:38081}` +- `WORDPRESS_SITE_URL=${AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT:-http://127.0.0.1:38082}` + +## Status And Shutdown + +Check runtime status: + +```bash +bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh +``` + +Stop the runtime: + +```bash +bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh +``` + +`status_mock_runtime.sh` reports postgres health plus each shim's PID/process state and `/healthz` result. `stop_mock_runtime.sh` stops the three shim processes and runs `docker compose down` for the local postgres stack. + +## Validation + +This phase does not include a dedicated smoke-test suite for the mock runtime. Validation is manual: + +- bring the runtime up with `start_mock_runtime.sh` +- confirm healthy output from `status_mock_runtime.sh` +- point AgentFlow at the three exported `AGENTFLOW_MCP_*_ENDPOINT` values +- verify requests against `canvas`, `notion`, and `woocommerce` end-to-end by hand diff --git a/sandbox/server/backends/resources/mcp/db/init.sql.gz b/sandbox/server/backends/resources/mcp/mock_runtime/db/init.sql.gz similarity index 100% rename from sandbox/server/backends/resources/mcp/db/init.sql.gz rename to sandbox/server/backends/resources/mcp/mock_runtime/db/init.sql.gz diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/docker-compose.yml b/sandbox/server/backends/resources/mcp/mock_runtime/docker-compose.yml new file mode 100644 index 00000000..dce41ede --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/docker-compose.yml @@ -0,0 +1,18 @@ +services: + postgres: + image: postgres:16 + environment: + POSTGRES_DB: ${POSTGRES_DB:-toolathlon_gym} + POSTGRES_USER: ${POSTGRES_USER:-eigent} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-camel} + ports: + - "${POSTGRES_HOST_PORT}:5432" + volumes: + - ./db/init.sql.gz:/docker-entrypoint-initdb.d/init.sql.gz:ro + healthcheck: + test: + - CMD-SHELL + - pg_isready -U ${POSTGRES_USER:-eigent} -d ${POSTGRES_DB:-toolathlon_gym} + interval: 5s + timeout: 5s + retries: 12 diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/common.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/common.sh new file mode 100644 index 00000000..80c07dbb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/common.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MOCK_RUNTIME_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ENV_FILE="$MOCK_RUNTIME_DIR/.env" +COMPOSE_FILE="$MOCK_RUNTIME_DIR/docker-compose.yml" +LOG_DIR="$MOCK_RUNTIME_DIR/logs" +RUN_DIR="$MOCK_RUNTIME_DIR/run" +NODE_BIN="${NODE_BIN:-node}" + +load_mock_runtime_env() { + if [[ ! -f "$ENV_FILE" ]]; then + echo "mock runtime env file not found: $ENV_FILE" >&2 + return 1 + fi + + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +} + +require_toolathlon_root() { + if [[ -z "${TOOLATHLON_GYM_ROOT:-}" ]]; then + echo "TOOLATHLON_GYM_ROOT is required" >&2 + return 1 + fi + + if [[ ! -d "$TOOLATHLON_GYM_ROOT" ]]; then + echo "TOOLATHLON_GYM_ROOT does not exist: $TOOLATHLON_GYM_ROOT" >&2 + return 1 + fi +} + +ensure_runtime_dirs() { + mkdir -p "$LOG_DIR" "$RUN_DIR" +} + +service_script_path() { + local service="$1" + printf '%s/shims/%s/server.mjs\n' "$MOCK_RUNTIME_DIR" "$service" +} + +service_pid_path() { + local service="$1" + background_pid_path "$service" +} + +service_log_path() { + local service="$1" + background_log_path "$service" +} + +background_pid_path() { + local name="$1" + printf '%s/%s.pid\n' "$RUN_DIR" "$name" +} + +background_log_path() { + local name="$1" + printf '%s/%s.log\n' "$LOG_DIR" "$name" +} + +service_host() { + local service="$1" + case "$service" in + canvas) printf '%s\n' "${CANVAS_SHIM_HOST:-127.0.0.1}" ;; + notion) printf '%s\n' "${NOTION_SHIM_HOST:-127.0.0.1}" ;; + woocommerce) printf '%s\n' "${WOOCOMMERCE_SHIM_HOST:-127.0.0.1}" ;; + *) + echo "unknown service: $service" >&2 + return 1 + ;; + esac +} + +service_port() { + local service="$1" + case "$service" in + canvas) printf '%s\n' "${CANVAS_SHIM_PORT:-38080}" ;; + notion) printf '%s\n' "${NOTION_SHIM_PORT:-38081}" ;; + woocommerce) printf '%s\n' "${WOOCOMMERCE_SHIM_PORT:-38082}" ;; + *) + echo "unknown service: $service" >&2 + return 1 + ;; + esac +} + +service_healthz_url() { + local service="$1" + local host + local port + host="$(service_host "$service")" || return 1 + port="$(service_port "$service")" || return 1 + + if [[ "$service" == "canvas" ]]; then + printf 'https://%s:%s/healthz\n' "$host" "$port" + return 0 + fi + + printf 'http://%s:%s/healthz\n' "$host" "$port" +} + +docker_compose() { + docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@" +} + +postgres_container_id() { + docker_compose ps -q postgres +} + +postgres_health_status() { + local container_id + container_id="$(postgres_container_id)" + if [[ -z "$container_id" ]]; then + printf 'not_running\n' + return 1 + fi + + docker inspect \ + --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \ + "$container_id" +} + +wait_for_postgres_healthy() { + local attempts="${1:-30}" + local sleep_seconds="${2:-2}" + local status="" + local attempt + + for ((attempt = 1; attempt <= attempts; attempt++)); do + status="$(postgres_health_status 2>/dev/null || true)" + if [[ "$status" == "healthy" ]]; then + return 0 + fi + sleep "$sleep_seconds" + done + + echo "postgres did not become healthy; last status: ${status:-unknown}" >&2 + return 1 +} + +is_pid_running() { + local pid="$1" + kill -0 "$pid" 2>/dev/null +} + +service_pid() { + local service="$1" + local pid_file + pid_file="$(service_pid_path "$service")" + if [[ -f "$pid_file" ]]; then + cat "$pid_file" + fi +} + +service_is_running() { + local service="$1" + local pid + pid="$(service_pid "$service")" + [[ -n "$pid" ]] && is_pid_running "$pid" +} + +start_service_process() { + local service="$1" + local script_path + + script_path="$(service_script_path "$service")" + + if [[ ! -f "$script_path" ]]; then + echo "service script not found: $script_path" >&2 + return 1 + fi + + if service_is_running "$service"; then + echo "$service is already running" >&2 + return 1 + fi + + start_node_background "$service" env TOOLATHLON_GYM_ROOT="$TOOLATHLON_GYM_ROOT" \ + "$NODE_BIN" "$script_path" +} + +start_node_background() { + local name="$1" + shift + + local pid_file + local log_file + local pid + + pid_file="$(background_pid_path "$name")" + log_file="$(background_log_path "$name")" + + rm -f "$pid_file" + ( + cd "$MOCK_RUNTIME_DIR" + nohup "$@" >>"$log_file" 2>&1 & + echo $! >"$pid_file" + ) + + pid="$(cat "$pid_file")" + if [[ -z "$pid" ]] || ! is_pid_running "$pid"; then + echo "failed to start $name" >&2 + return 1 + fi +} + +stop_service_process() { + local service="$1" + local pid_file + local pid + + pid_file="$(service_pid_path "$service")" + if [[ ! -f "$pid_file" ]]; then + return 0 + fi + + pid="$(cat "$pid_file")" + if [[ -n "$pid" ]] && is_pid_running "$pid"; then + kill "$pid" + for _ in {1..10}; do + if ! is_pid_running "$pid"; then + break + fi + sleep 1 + done + if is_pid_running "$pid"; then + kill -9 "$pid" + fi + fi + + rm -f "$pid_file" +} + +probe_service_healthz() { + local service="$1" + local url + local curl_args=(-fsS --max-time 5) + + url="$(service_healthz_url "$service")" || return 1 + if [[ "$service" == "canvas" ]]; then + curl_args+=(-k) + fi + + curl "${curl_args[@]}" "$url" >/dev/null +} + +wait_for_service_healthz() { + local service="$1" + local attempts="${2:-20}" + local sleep_seconds="${3:-1}" + local attempt + + for ((attempt = 1; attempt <= attempts; attempt++)); do + if probe_service_healthz "$service"; then + return 0 + fi + sleep "$sleep_seconds" + done + + echo "$service health check failed" >&2 + return 1 +} diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh new file mode 100644 index 00000000..3da4da01 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/common.sh" + +SERVICES=(canvas notion woocommerce) +STARTED_SERVICES=() +POSTGRES_STARTED_BY_SCRIPT=0 + +cleanup_on_failure() { + local service + for ((idx = ${#STARTED_SERVICES[@]} - 1; idx >= 0; idx--)); do + service="${STARTED_SERVICES[$idx]}" + stop_service_process "$service" || true + done + + if [[ "$POSTGRES_STARTED_BY_SCRIPT" -eq 1 ]]; then + docker_compose stop postgres || true + fi +} + +trap cleanup_on_failure ERR + +load_mock_runtime_env +require_toolathlon_root +ensure_runtime_dirs + +if [[ -z "$(postgres_container_id)" ]]; then + POSTGRES_STARTED_BY_SCRIPT=1 +fi + +docker_compose up -d postgres +wait_for_postgres_healthy + +for service in "${SERVICES[@]}"; do + start_service_process "$service" + STARTED_SERVICES+=("$service") + wait_for_service_healthz "$service" +done + +trap - ERR + +echo "mock runtime started" diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh new file mode 100644 index 00000000..05ea7e30 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/common.sh" + +SERVICES=(canvas notion woocommerce) +overall_status=0 + +load_mock_runtime_env +ensure_runtime_dirs + +postgres_status="$(postgres_health_status 2>/dev/null || true)" +if [[ -z "$postgres_status" ]]; then + postgres_status="not_running" + overall_status=1 +elif [[ "$postgres_status" != "healthy" ]]; then + overall_status=1 +fi +echo "postgres: $postgres_status" + +for service in "${SERVICES[@]}"; do + pid="$(service_pid "$service")" + if [[ -n "$pid" ]] && is_pid_running "$pid"; then + process_status="running" + elif [[ -n "$pid" ]]; then + process_status="stale_pid" + overall_status=1 + else + process_status="not_running" + overall_status=1 + fi + + if probe_service_healthz "$service"; then + health_status="healthy" + else + health_status="unhealthy" + overall_status=1 + fi + + echo "$service: pid=${pid:-none} process=$process_status healthz=$health_status" +done + +exit "$overall_status" diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh new file mode 100644 index 00000000..cbb5222d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/common.sh" + +SERVICES=(canvas notion woocommerce) + +load_mock_runtime_env +ensure_runtime_dirs + +for service in "${SERVICES[@]}"; do + stop_service_process "$service" +done + +docker_compose down + +echo "mock runtime stopped" diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.mjs new file mode 100644 index 00000000..a8f8f5cc --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.mjs @@ -0,0 +1,16 @@ +const MOCK_CANVAS_PREFIX = "https://mock-canvas.local/"; + +export function rewriteCanvasPublicUrl(url, publicBaseUrl) { + if (typeof url !== "string" || !url.startsWith(MOCK_CANVAS_PREFIX)) { + return url; + } + + const sourceUrl = new URL(url); + const targetUrl = new URL(publicBaseUrl); + + targetUrl.pathname = sourceUrl.pathname; + targetUrl.search = sourceUrl.search; + targetUrl.hash = sourceUrl.hash; + + return targetUrl.toString(); +} diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.test.mjs new file mode 100644 index 00000000..f50b29a6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.test.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { rewriteCanvasPublicUrl } from "./rewrite_urls.mjs"; + +test("rewrites mock canvas upload URLs to the public shim base URL", () => { + assert.equal( + rewriteCanvasPublicUrl( + "https://mock-canvas.local/upload/123", + "https://127.0.0.1:38080", + ), + "https://127.0.0.1:38080/upload/123", + ); +}); + +test("leaves normal URLs unchanged", () => { + const url = "https://example.com/upload/123?x=1"; + + assert.equal( + rewriteCanvasPublicUrl(url, "https://127.0.0.1:38080"), + url, + ); +}); diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.mjs new file mode 100644 index 00000000..71efd87a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.mjs @@ -0,0 +1,396 @@ +import fs from "node:fs"; +import https from "node:https"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { rewriteCanvasPublicUrl } from "./rewrite_urls.mjs"; + +function resolveToolathlonGymRoot() { + const root = process.env.TOOLATHLON_GYM_ROOT; + if (!root) { + throw new Error("TOOLATHLON_GYM_ROOT is required"); + } + + return path.resolve(root); +} + +function deriveRouterEnv() { + process.env.PG_HOST ||= "127.0.0.1"; + process.env.PG_PORT ||= process.env.POSTGRES_HOST_PORT || "5432"; + process.env.PG_DATABASE ||= process.env.POSTGRES_DB || "toolathlon"; + process.env.PG_USER ||= process.env.POSTGRES_USER || "postgres"; + process.env.PG_PASSWORD ||= process.env.POSTGRES_PASSWORD || "postgres"; +} + +async function loadPgCanvasRouter() { + const gymRoot = resolveToolathlonGymRoot(); + const routerModulePath = path.join( + gymRoot, + "local_servers/mcp-canvas-lms/build/pg-canvas-router.js", + ); + + if (!fs.existsSync(routerModulePath)) { + throw new Error(`PgCanvasRouter build not found: ${routerModulePath}`); + } + + const routerModule = await import(pathToFileURL(routerModulePath).href); + if (!routerModule.PgCanvasRouter) { + throw new Error(`PgCanvasRouter export missing: ${routerModulePath}`); + } + + return routerModule.PgCanvasRouter; +} + +function resolveRuntimePath(filePath) { + return path.isAbsolute(filePath) + ? filePath + : path.resolve(process.cwd(), filePath); +} + +function getPublicBaseUrl() { + const host = process.env.CANVAS_SHIM_HOST || "127.0.0.1"; + const port = Number(process.env.CANVAS_SHIM_PORT || "38080"); + return `https://${host}:${port}`; +} + +function getServerOptions() { + const certPath = process.env.CANVAS_TLS_CERT_PATH; + const keyPath = process.env.CANVAS_TLS_KEY_PATH; + + if (!certPath || !keyPath) { + throw new Error("CANVAS_TLS_CERT_PATH and CANVAS_TLS_KEY_PATH are required"); + } + + return { + host: process.env.CANVAS_SHIM_HOST || "127.0.0.1", + port: Number(process.env.CANVAS_SHIM_PORT || "38080"), + cert: fs.readFileSync(resolveRuntimePath(certPath)), + key: fs.readFileSync(resolveRuntimePath(keyPath)), + }; +} + +function sendJson(res, statusCode, payload) { + res.writeHead(statusCode, { "content-type": "application/json" }); + res.end(JSON.stringify(payload)); +} + +function collectQueryParams(searchParams) { + const params = {}; + + for (const [key, value] of searchParams) { + if (params[key] === undefined) { + params[key] = value; + continue; + } + + params[key] = Array.isArray(params[key]) + ? [...params[key], value] + : [params[key], value]; + } + + return params; +} + +async function readRequestBody(req) { + const chunks = []; + + for await (const chunk of req) { + chunks.push(chunk); + } + + return Buffer.concat(chunks); +} + +async function drainRequestBody(req) { + for await (const _chunk of req) { + // Drain upload bodies without buffering them in memory. + } +} + +async function parseRouterBody(req) { + const bodyBuffer = await readRequestBody(req); + if (bodyBuffer.length === 0) { + return {}; + } + + const contentType = req.headers["content-type"] || ""; + const rawBody = bodyBuffer.toString("utf8"); + + if (contentType.includes("application/json")) { + return JSON.parse(rawBody); + } + + if (contentType.includes("application/x-www-form-urlencoded")) { + return Object.fromEntries(new URLSearchParams(rawBody)); + } + + try { + return JSON.parse(rawBody); + } catch { + return {}; + } +} + +function rewriteCanvasPayload(value, publicBaseUrl) { + if (Array.isArray(value)) { + return value.map((item) => rewriteCanvasPayload(item, publicBaseUrl)); + } + + if (!value || typeof value !== "object") { + return value; + } + + const rewritten = {}; + + for (const [key, nestedValue] of Object.entries(value)) { + if ( + (key === "upload_url" || key === "location" || key === "url") + && typeof nestedValue === "string" + ) { + rewritten[key] = rewriteCanvasPublicUrl(nestedValue, publicBaseUrl); + continue; + } + + rewritten[key] = rewriteCanvasPayload(nestedValue, publicBaseUrl); + } + + return rewritten; +} + +function rememberUploadMapping(payload, publicBaseUrl, uploadMappings) { + if ( + !payload + || typeof payload !== "object" + || Array.isArray(payload) + || payload.id === undefined + || typeof payload.upload_url !== "string" + ) { + return; + } + + const uploadUrl = new URL( + rewriteCanvasPublicUrl(payload.upload_url, publicBaseUrl), + ); + + uploadMappings.set(uploadUrl.pathname, String(payload.id)); +} + +function rememberFileMapping(payload, publicBaseUrl, fileMappings) { + if ( + !payload + || typeof payload !== "object" + || Array.isArray(payload) + || payload.id === undefined + || typeof payload.url !== "string" + ) { + return; + } + + const fileUrl = new URL( + rewriteCanvasPublicUrl(payload.url, publicBaseUrl), + ); + const publicOrigin = new URL(publicBaseUrl).origin; + + if ( + fileUrl.origin !== publicOrigin + || !fileUrl.pathname.startsWith("/files/") + ) { + return; + } + + fileMappings.set(fileUrl.pathname, String(payload.id)); +} + +async function forwardToCanvasRouter( + req, + res, + requestUrl, + router, + publicBaseUrl, + uploadMappings, + fileMappings, +) { + const canvasPath = requestUrl.pathname.replace(/^\/api\/v1\/?/, ""); + const queryParams = collectQueryParams(requestUrl.searchParams); + const method = req.method || "GET"; + + let routerResponse; + + if (method === "GET") { + routerResponse = await router.get(canvasPath, { params: queryParams }); + } else if (method === "POST") { + routerResponse = await router.post( + canvasPath, + await parseRouterBody(req), + { params: queryParams }, + ); + } else if (method === "PUT") { + routerResponse = await router.put( + canvasPath, + await parseRouterBody(req), + { params: queryParams }, + ); + } else if (method === "DELETE") { + routerResponse = await router.delete(canvasPath, { + data: await parseRouterBody(req), + params: queryParams, + }); + } else { + sendJson(res, 405, { error: "method_not_allowed", method }); + return; + } + + const payload = rewriteCanvasPayload(routerResponse.data, publicBaseUrl); + rememberUploadMapping(payload, publicBaseUrl, uploadMappings); + rememberFileMapping(payload, publicBaseUrl, fileMappings); + sendJson(res, routerResponse.status || 200, payload); +} + +async function handleUploadRequest(req, res, requestUrl, publicBaseUrl, uploadMappings) { + if (req.method !== "POST") { + sendJson(res, 405, { error: "method_not_allowed", method: req.method }); + return; + } + + const fileId = uploadMappings.get(requestUrl.pathname); + if (!fileId) { + sendJson(res, 404, { error: "upload_not_found", path: requestUrl.pathname }); + return; + } + + await drainRequestBody(req); + + sendJson(res, 200, { + location: new URL(`/api/v1/files/${fileId}`, publicBaseUrl).toString(), + }); +} + +async function handleFileRequest(req, res, requestUrl, router, publicBaseUrl, fileMappings) { + if (req.method !== "GET") { + sendJson(res, 405, { error: "method_not_allowed", method: req.method }); + return; + } + + const fileId = fileMappings.get(requestUrl.pathname); + if (!fileId) { + sendJson(res, 404, { error: "file_not_found", path: requestUrl.pathname }); + return; + } + + const routerResponse = await router.get(`files/${fileId}`, { + params: collectQueryParams(requestUrl.searchParams), + }); + const payload = rewriteCanvasPayload(routerResponse.data, publicBaseUrl); + rememberFileMapping(payload, publicBaseUrl, fileMappings); + sendJson(res, routerResponse.status || 200, payload); +} + +export async function createCanvasShimServer() { + deriveRouterEnv(); + + const PgCanvasRouter = await loadPgCanvasRouter(); + const router = new PgCanvasRouter(); + const uploadMappings = new Map(); + const fileMappings = new Map(); + const publicBaseUrl = getPublicBaseUrl(); + const serverOptions = getServerOptions(); + + const server = https.createServer(serverOptions, async (req, res) => { + try { + const requestUrl = new URL(req.url || "/", publicBaseUrl); + + if (req.method === "GET" && requestUrl.pathname === "/healthz") { + sendJson(res, 200, { ok: true }); + return; + } + + if (requestUrl.pathname.startsWith("/upload/")) { + await handleUploadRequest( + req, + res, + requestUrl, + publicBaseUrl, + uploadMappings, + ); + return; + } + + if (requestUrl.pathname.startsWith("/files/")) { + await handleFileRequest( + req, + res, + requestUrl, + router, + publicBaseUrl, + fileMappings, + ); + return; + } + + if (requestUrl.pathname.startsWith("/api/v1/")) { + await forwardToCanvasRouter( + req, + res, + requestUrl, + router, + publicBaseUrl, + uploadMappings, + fileMappings, + ); + return; + } + + sendJson(res, 404, { error: "not_found", path: requestUrl.pathname }); + } catch (error) { + sendJson(res, 500, { error: String(error) }); + } + }); + + return { server, router, host: serverOptions.host, port: serverOptions.port }; +} + +export const __test__ = { + drainRequestBody, + handleFileRequest, +}; + +export async function startCanvasShimServer() { + const { server, router, host, port } = await createCanvasShimServer(); + + await new Promise((resolve) => { + server.listen(port, host, resolve); + }); + + return { server, router }; +} + +async function shutdown(server, router, code = 0) { + server.close(() => { + router.pool?.end?.().finally(() => { + process.exit(code); + }); + }); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + const { server, router } = await startCanvasShimServer(); + const address = server.address(); + + if (!address || typeof address === "string") { + throw new Error("Canvas shim failed to bind a TCP port"); + } + + console.log(`READY ${address.port}`); + + process.on("SIGTERM", () => { + shutdown(server, router, 0); + }); + process.on("SIGINT", () => { + shutdown(server, router, 0); + }); + } catch (error) { + console.error(String(error)); + process.exit(1); + } +} diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.test.mjs new file mode 100644 index 00000000..6497b4a2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.test.mjs @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { Readable } from "node:stream"; +import test from "node:test"; + +import { __test__ } from "./server.mjs"; + +function createMockResponse() { + return { + body: "", + headers: undefined, + statusCode: undefined, + end(chunk = "") { + this.body = String(chunk); + }, + writeHead(statusCode, headers) { + this.statusCode = statusCode; + this.headers = headers; + }, + }; +} + +test("bridges rewritten file URLs to the router GET files/:id endpoint", async () => { + const response = createMockResponse(); + const routerCalls = []; + const fileMappings = new Map([["/files/report.pdf", "file-42"]]); + const publicBaseUrl = "https://127.0.0.1:38080"; + + await __test__.handleFileRequest( + { method: "GET" }, + response, + new URL("/files/report.pdf", publicBaseUrl), + { + async get(path, options) { + routerCalls.push({ path, options }); + return { + status: 200, + data: { + id: "file-42", + url: "https://mock-canvas.local/files/report.pdf", + }, + }; + }, + }, + publicBaseUrl, + fileMappings, + ); + + assert.deepEqual(routerCalls, [{ + path: "files/file-42", + options: { params: {} }, + }]); + assert.equal(response.statusCode, 200); + assert.deepEqual(JSON.parse(response.body), { + id: "file-42", + url: "https://127.0.0.1:38080/files/report.pdf", + }); +}); + +test("drains upload request streams without concatenating buffered chunks", async () => { + const originalConcat = Buffer.concat; + + Buffer.concat = () => { + throw new Error("Buffer.concat should not be used while draining uploads"); + }; + + try { + await __test__.drainRequestBody(Readable.from([ + Buffer.from("chunk-1"), + Buffer.from("chunk-2"), + ])); + } finally { + Buffer.concat = originalConcat; + } +}); diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.mjs new file mode 100644 index 00000000..52b65c09 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.mjs @@ -0,0 +1,120 @@ +function normalizePathname(pathname) { + if (!pathname) { + return "/"; + } + + if (pathname.length > 1 && pathname.endsWith("/")) { + return pathname.slice(0, -1); + } + + return pathname; +} + +function splitPathSegments(pathname) { + return normalizePathname(pathname) + .split("/") + .filter(Boolean); +} + +function isPlaceholderSegment(segment) { + return segment.startsWith("{") && segment.endsWith("}"); +} + +function getPlaceholderName(segment) { + return segment.slice(1, -1); +} + +function matchPath(templatePath, requestPath) { + const templateSegments = splitPathSegments(templatePath); + const requestSegments = splitPathSegments(requestPath); + + if (templateSegments.length !== requestSegments.length) { + return null; + } + + const pathParams = {}; + let literalCount = 0; + + for (let index = 0; index < templateSegments.length; index += 1) { + const templateSegment = templateSegments[index]; + const requestSegment = requestSegments[index]; + + if (isPlaceholderSegment(templateSegment)) { + pathParams[getPlaceholderName(templateSegment)] = decodeURIComponent( + requestSegment, + ); + continue; + } + + if (templateSegment !== requestSegment) { + return null; + } + + literalCount += 1; + } + + return { + literalCount, + pathParams, + }; +} + +export function lookupOperation(spec, method, pathname) { + const paths = spec?.paths; + if (!paths || !method) { + return null; + } + + const normalizedPath = normalizePathname(pathname); + const normalizedMethod = method.toLowerCase(); + const exactPathItem = paths[normalizedPath]; + const exactOperation = exactPathItem?.[normalizedMethod]; + + if (exactOperation) { + return { + operation: exactOperation, + pathParams: {}, + }; + } + + let bestMatch = null; + + for (const [templatePath, pathItem] of Object.entries(paths)) { + const operation = pathItem?.[normalizedMethod]; + if (!operation) { + continue; + } + + const matchedPath = matchPath(templatePath, normalizedPath); + if (!matchedPath) { + continue; + } + + const candidate = { + operation, + pathParams: matchedPath.pathParams, + score: matchedPath.literalCount, + templatePath, + }; + + if ( + !bestMatch + || candidate.score > bestMatch.score + || ( + candidate.score === bestMatch.score + && candidate.templatePath < bestMatch.templatePath + ) + ) { + bestMatch = candidate; + } + } + + if (!bestMatch) { + return null; + } + + return { + operation: bestMatch.operation, + pathParams: bestMatch.pathParams, + }; +} diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.test.mjs new file mode 100644 index 00000000..6b4edd6a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.test.mjs @@ -0,0 +1,57 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { lookupOperation } from "./lookup_operation.mjs"; + +const spec = { + paths: { + "/v1/users/{user_id}": { + get: { + operationId: "get-user", + }, + }, + "/v1/users": { + get: { + operationId: "get-users", + }, + }, + "/v1/users/me": { + get: { + operationId: "get-self", + }, + }, + "/v1/databases/{database_id}/query": { + post: { + operationId: "post-database-query", + }, + }, + }, +}; + +test("prefers exact notion route matches over placeholder routes", () => { + const match = lookupOperation(spec, "GET", "/v1/users/me"); + + assert.deepEqual(match, { + operation: { + operationId: "get-self", + }, + pathParams: {}, + }); +}); + +test("matches notion placeholder routes and extracts path params", () => { + const match = lookupOperation(spec, "POST", "/v1/databases/abc/query"); + + assert.deepEqual(match, { + operation: { + operationId: "post-database-query", + }, + pathParams: { + database_id: "abc", + }, + }); +}); + +test("returns null when no notion operation matches the request", () => { + assert.equal(lookupOperation(spec, "DELETE", "/v1/users/me"), null); +}); diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.mjs new file mode 100644 index 00000000..d5daafcd --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.mjs @@ -0,0 +1,263 @@ +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { lookupOperation } from "./lookup_operation.mjs"; + +function resolveToolathlonGymRoot() { + const root = process.env.TOOLATHLON_GYM_ROOT; + if (!root) { + throw new Error("TOOLATHLON_GYM_ROOT is required"); + } + + return path.resolve(root); +} + +function deriveClientEnv() { + process.env.PG_HOST ||= "127.0.0.1"; + process.env.PG_PORT ||= process.env.POSTGRES_HOST_PORT || "5432"; + process.env.PG_DATABASE ||= process.env.POSTGRES_DB || "toolathlon_gym"; + process.env.PG_USER ||= process.env.POSTGRES_USER || "eigent"; + process.env.PG_PASSWORD ||= process.env.POSTGRES_PASSWORD || "camel"; +} + +function getServerOptions() { + return { + host: process.env.NOTION_SHIM_HOST || "127.0.0.1", + port: Number(process.env.NOTION_SHIM_PORT || "38081"), + }; +} + +function sendJson(res, statusCode, payload) { + res.writeHead(statusCode, { "content-type": "application/json" }); + res.end(JSON.stringify(payload)); +} + +function collectQueryParams(searchParams) { + const params = {}; + + for (const [key, value] of searchParams) { + if (params[key] === undefined) { + params[key] = value; + continue; + } + + params[key] = Array.isArray(params[key]) + ? [...params[key], value] + : [params[key], value]; + } + + return params; +} + +async function readRequestBody(req) { + const chunks = []; + + for await (const chunk of req) { + chunks.push(chunk); + } + + return Buffer.concat(chunks).toString("utf8"); +} + +async function parseJsonBody(req) { + const rawBody = await readRequestBody(req); + if (!rawBody) { + return {}; + } + + try { + const parsed = JSON.parse(rawBody); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? parsed + : {}; + } catch (error) { + if (error instanceof SyntaxError) { + error.code = "invalid_json"; + } + + throw error; + } +} + +function buildOperationParams(pathParams, queryParams, bodyParams) { + return { + ...queryParams, + ...bodyParams, + ...pathParams, + }; +} + +function extractResponsePayload(result) { + if (result?.body !== undefined) { + return result.body; + } + + if (result?.data !== undefined) { + return result.data; + } + + return {}; +} + +function getOperationStatus(result) { + return Number.isInteger(result?.status) ? result.status : 200; +} + +function getOperationResponse(result) { + return { + payload: extractResponsePayload(result), + status: getOperationStatus(result), + }; +} + +function loadOpenApiSpec() { + const gymRoot = resolveToolathlonGymRoot(); + const specPath = path.join( + gymRoot, + "local_servers/notion-mcp-server/scripts/notion-openapi.json", + ); + + if (!fs.existsSync(specPath)) { + throw new Error(`Notion OpenAPI spec not found: ${specPath}`); + } + + return JSON.parse(fs.readFileSync(specPath, "utf8")); +} + +async function loadPgHttpClient() { + const gymRoot = resolveToolathlonGymRoot(); + const clientModulePath = path.join( + gymRoot, + "local_servers/notion-mcp-server/build/src/openapi-mcp-server/client/pg-client.js", + ); + + if (!fs.existsSync(clientModulePath)) { + throw new Error(`PgHttpClient build not found: ${clientModulePath}`); + } + + const clientModule = await import(pathToFileURL(clientModulePath).href); + const PgHttpClient = clientModule.PgHttpClient + || clientModule.default?.PgHttpClient + || clientModule.default; + + if (typeof PgHttpClient !== "function") { + throw new Error(`PgHttpClient export missing: ${clientModulePath}`); + } + + return PgHttpClient; +} + +async function forwardToNotionClient(req, res, requestUrl, spec, client) { + const match = lookupOperation(spec, req.method || "GET", requestUrl.pathname); + + if (!match) { + sendJson(res, 404, { error: "not_found", path: requestUrl.pathname }); + return; + } + + const bodyParams = ["POST", "PATCH", "PUT"].includes(req.method || "") + ? await parseJsonBody(req) + : {}; + const params = buildOperationParams( + match.pathParams, + collectQueryParams(requestUrl.searchParams), + bodyParams, + ); + const result = await client.executeOperation(match.operation, params); + const response = getOperationResponse(result); + + sendJson(res, response.status, response.payload); +} + +export async function createNotionShimServer() { + deriveClientEnv(); + + const [spec, PgHttpClient] = await Promise.all([ + loadOpenApiSpec(), + loadPgHttpClient(), + ]); + const client = new PgHttpClient(); + const serverOptions = getServerOptions(); + const baseUrl = `http://${serverOptions.host}:${serverOptions.port}`; + + const server = http.createServer(async (req, res) => { + try { + const requestUrl = new URL(req.url || "/", baseUrl); + + if (req.method === "GET" && requestUrl.pathname === "/healthz") { + sendJson(res, 200, { ok: true }); + return; + } + + if (requestUrl.pathname.startsWith("/v1/")) { + await forwardToNotionClient(req, res, requestUrl, spec, client); + return; + } + + sendJson(res, 404, { error: "not_found", path: requestUrl.pathname }); + } catch (error) { + if (error instanceof SyntaxError && error.code === "invalid_json") { + sendJson(res, 400, { error: "invalid_json" }); + return; + } + + console.error(error); + sendJson(res, 500, { error: "internal_error" }); + } + }); + + return { + client, + host: serverOptions.host, + port: serverOptions.port, + server, + }; +} + +export async function startNotionShimServer() { + const { server, client, host, port } = await createNotionShimServer(); + + await new Promise((resolve) => { + server.listen(port, host, resolve); + }); + + return { client, server }; +} + +export const __test__ = { + buildOperationParams, +}; + +async function shutdown(server, client, code = 0) { + server.close(() => { + Promise.resolve(client.pool?.end?.()) + .finally(() => { + process.exit(code); + }); + }); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + const { server, client } = await startNotionShimServer(); + const address = server.address(); + + if (!address || typeof address === "string") { + throw new Error("Notion shim failed to bind a TCP port"); + } + + console.log(`READY ${address.port}`); + + process.on("SIGTERM", () => { + shutdown(server, client, 0); + }); + process.on("SIGINT", () => { + shutdown(server, client, 0); + }); + } catch (error) { + console.error(String(error)); + process.exit(1); + } +} diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.test.mjs new file mode 100644 index 00000000..005583ed --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.test.mjs @@ -0,0 +1,124 @@ +import fs from "node:fs"; +import assert from "node:assert/strict"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import * as serverModule from "./server.mjs"; + +test("path params override query params and body params", () => { + const params = serverModule.__test__?.buildOperationParams( + { database_id: "abc", user_id: "123" }, + { user_id: "456", filter: "active" }, + { database_id: "override", page_size: 10 }, + ); + + assert.deepEqual(params, { + database_id: "abc", + filter: "active", + page_size: 10, + user_id: "123", + }); +}); + +test("internal server errors stay opaque to clients", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "notion-shim-")); + fs.mkdirSync( + path.join( + root, + "local_servers/notion-mcp-server/scripts", + ), + { recursive: true }, + ); + fs.mkdirSync( + path.join( + root, + "local_servers/notion-mcp-server/build/src/openapi-mcp-server/client", + ), + { recursive: true }, + ); + + fs.writeFileSync( + path.join(root, "package.json"), + JSON.stringify({ type: "module" }), + ); + fs.writeFileSync( + path.join( + root, + "local_servers/notion-mcp-server/scripts/notion-openapi.json", + ), + JSON.stringify({ paths: { "/v1/test": { get: {} } } }), + ); + fs.writeFileSync( + path.join( + root, + "local_servers/notion-mcp-server/build/src/openapi-mcp-server/client/pg-client.js", + ), + "export class PgHttpClient { async executeOperation() { throw new Error('boom: secret detail'); } }\n", + ); + + const previousEnv = { + TOOLATHLON_GYM_ROOT: process.env.TOOLATHLON_GYM_ROOT, + NOTION_SHIM_HOST: process.env.NOTION_SHIM_HOST, + NOTION_SHIM_PORT: process.env.NOTION_SHIM_PORT, + }; + process.env.TOOLATHLON_GYM_ROOT = root; + process.env.NOTION_SHIM_HOST = "127.0.0.1"; + process.env.NOTION_SHIM_PORT = "0"; + + const consoleErrors = []; + const originalConsoleError = console.error; + console.error = (...args) => { + consoleErrors.push(args); + }; + + let server; + try { + ({ server } = await serverModule.createNotionShimServer()); + + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", resolve); + }); + + const address = server.address(); + assert.ok(address && typeof address !== "string"); + + const response = await new Promise((resolve, reject) => { + const req = http.request( + { + hostname: "127.0.0.1", + port: address.port, + path: "/v1/test", + method: "GET", + }, + (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => { + resolve({ + body: Buffer.concat(chunks).toString("utf8"), + statusCode: res.statusCode, + }); + }); + }, + ); + + req.on("error", reject); + req.end(); + }); + + assert.equal(response.statusCode, 500); + assert.deepEqual(JSON.parse(response.body), { error: "internal_error" }); + assert.equal(consoleErrors.length > 0, true); + assert.match(String(consoleErrors[0][0]), /boom: secret detail/); + } finally { + console.error = originalConsoleError; + process.env.TOOLATHLON_GYM_ROOT = previousEnv.TOOLATHLON_GYM_ROOT; + process.env.NOTION_SHIM_HOST = previousEnv.NOTION_SHIM_HOST; + process.env.NOTION_SHIM_PORT = previousEnv.NOTION_SHIM_PORT; + await new Promise((resolve) => { + server?.close(resolve); + }); + } +}); diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.mjs new file mode 100644 index 00000000..70341e37 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.mjs @@ -0,0 +1,295 @@ +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { stripWooPrefix } from "./strip_prefix.mjs"; + +function resolveToolathlonGymRoot() { + const root = process.env.TOOLATHLON_GYM_ROOT; + if (!root) { + throw new Error("TOOLATHLON_GYM_ROOT is required"); + } + + return path.resolve(root); +} + +function deriveRouterEnv() { + return { + PG_HOST: process.env.PG_HOST || "127.0.0.1", + PG_PORT: process.env.PG_PORT || process.env.POSTGRES_HOST_PORT || "5432", + PG_DATABASE: process.env.PG_DATABASE || process.env.POSTGRES_DB || "toolathlon_gym", + PG_USER: process.env.PG_USER || process.env.POSTGRES_USER || "eigent", + PG_PASSWORD: process.env.PG_PASSWORD || process.env.POSTGRES_PASSWORD || "camel", + }; +} + +async function withRouterEnv(callback) { + const derivedEnv = deriveRouterEnv(); + const previousEnv = {}; + + for (const [key, value] of Object.entries(derivedEnv)) { + previousEnv[key] = process.env[key]; + process.env[key] = value; + } + + try { + return await callback(); + } finally { + for (const [key, value] of Object.entries(previousEnv)) { + if (value === undefined) { + delete process.env[key]; + continue; + } + + process.env[key] = value; + } + } +} + +async function loadPgRestRouter() { + const gymRoot = resolveToolathlonGymRoot(); + const routerModulePath = path.join( + gymRoot, + "local_servers/woocommerce-mcp/dist/services/pg-rest-router.js", + ); + + if (!fs.existsSync(routerModulePath)) { + throw new Error(`PgRestRouter build not found: ${routerModulePath}`); + } + + const routerModule = await import(pathToFileURL(routerModulePath).href); + const PgRestRouter = routerModule.PgRestRouter + || routerModule.default?.PgRestRouter + || routerModule.default; + + if (typeof PgRestRouter !== "function") { + throw new Error(`PgRestRouter export missing: ${routerModulePath}`); + } + + return PgRestRouter; +} + +function getServerOptions() { + return { + host: process.env.WOOCOMMERCE_SHIM_HOST || "127.0.0.1", + port: Number(process.env.WOOCOMMERCE_SHIM_PORT || "38082"), + }; +} + +function sendJson(res, statusCode, payload) { + res.writeHead(statusCode, { "content-type": "application/json" }); + res.end(JSON.stringify(payload)); +} + +function collectQueryParams(searchParams) { + const params = {}; + + for (const [key, value] of searchParams) { + if (params[key] === undefined) { + params[key] = value; + continue; + } + + params[key] = Array.isArray(params[key]) + ? [...params[key], value] + : [params[key], value]; + } + + return params; +} + +async function readRequestBody(req) { + const chunks = []; + + for await (const chunk of req) { + chunks.push(chunk); + } + + return Buffer.concat(chunks).toString("utf8"); +} + +async function parseJsonBody(req) { + const rawBody = await readRequestBody(req); + if (!rawBody) { + return undefined; + } + + try { + return JSON.parse(rawBody); + } catch (error) { + if (error instanceof SyntaxError) { + error.code = "invalid_json"; + } + + throw error; + } +} + +async function forwardToWooRouter(req, res, requestUrl, router) { + const method = req.method || "GET"; + const routerPath = stripWooPrefix(requestUrl.pathname); + const params = collectQueryParams(requestUrl.searchParams); + + let routerResponse; + + if (method === "GET") { + routerResponse = await router.get(routerPath, { params }); + } else if (method === "POST") { + routerResponse = await router.post( + routerPath, + await parseJsonBody(req), + { params }, + ); + } else if (method === "PUT") { + routerResponse = await router.put( + routerPath, + await parseJsonBody(req), + { params }, + ); + } else if (method === "DELETE") { + const data = await parseJsonBody(req); + const config = { params }; + + if (data !== undefined) { + config.data = data; + } + + routerResponse = await router.delete(routerPath, config); + } else { + sendJson(res, 405, { error: "method_not_allowed", method }); + return; + } + + sendJson(res, routerResponse.status || 200, routerResponse.data); +} + +export async function createWooCommerceShimServer() { + const { router } = await withRouterEnv(async () => { + const LoadedPgRestRouter = await loadPgRestRouter(); + return { + router: new LoadedPgRestRouter(), + }; + }); + const serverOptions = getServerOptions(); + const baseUrl = `http://${serverOptions.host}:${serverOptions.port}`; + + const server = http.createServer(async (req, res) => { + try { + const requestUrl = new URL(req.url || "/", baseUrl); + + if (req.method === "GET" && requestUrl.pathname === "/healthz") { + sendJson(res, 200, { ok: true }); + return; + } + + if (requestUrl.pathname.startsWith("/wp-json/wc/v3/")) { + await forwardToWooRouter(req, res, requestUrl, router); + return; + } + + sendJson(res, 404, { error: "not_found", path: requestUrl.pathname }); + } catch (error) { + if (error instanceof SyntaxError && error.code === "invalid_json") { + sendJson(res, 400, { error: "invalid_json" }); + return; + } + + console.error(error); + sendJson(res, 500, { error: "internal_error" }); + } + }); + + return { + host: serverOptions.host, + port: serverOptions.port, + router, + server, + }; +} + +async function cleanupStartFailure(server, router) { + await new Promise((resolve) => { + try { + server.close(() => { + resolve(); + }); + } catch { + resolve(); + } + }); + + try { + await router.pool?.end?.(); + } catch { + // Best-effort cleanup for startup failures. + } +} + +export async function startWooCommerceShimServer() { + const { server, router, host, port } = await createWooCommerceShimServer(); + + await new Promise((resolve, reject) => { + const rejectWithCleanup = (error) => { + server.off("listening", handleListening); + server.off("error", handleError); + cleanupStartFailure(server, router).finally(() => { + reject(error); + }); + }; + const handleError = (error) => { + rejectWithCleanup(error); + }; + const handleListening = () => { + server.off("error", handleError); + resolve(); + }; + + server.once("error", handleError); + server.once("listening", handleListening); + + try { + server.listen(port, host); + } catch (error) { + server.off("error", handleError); + server.off("listening", handleListening); + cleanupStartFailure(server, router).finally(() => { + reject(error); + }); + } + }); + + return { router, server }; +} + +async function shutdown(server, router, code = 0) { + server.close(() => { + Promise.resolve(router.pool?.end?.()) + .finally(() => { + process.exit(code); + }); + }); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + const { server, router } = await startWooCommerceShimServer(); + const address = server.address(); + + if (!address || typeof address === "string") { + throw new Error("WooCommerce shim failed to bind a TCP port"); + } + + console.log(`READY ${address.port}`); + + process.on("SIGTERM", () => { + shutdown(server, router, 0); + }); + process.on("SIGINT", () => { + shutdown(server, router, 0); + }); + } catch (error) { + console.error(String(error)); + process.exit(1); + } +} diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.test.mjs new file mode 100644 index 00000000..60096599 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.test.mjs @@ -0,0 +1,412 @@ +import assert from "node:assert/strict"; +import { EventEmitter } from "node:events"; +import fs from "node:fs"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import * as serverModule from "./server.mjs"; + +function createGymRoot(routerSource) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "woocommerce-shim-")); + + fs.mkdirSync( + path.join( + root, + "local_servers/woocommerce-mcp/dist/services", + ), + { recursive: true }, + ); + fs.writeFileSync( + path.join(root, "package.json"), + JSON.stringify({ type: "module" }), + ); + fs.writeFileSync( + path.join( + root, + "local_servers/woocommerce-mcp/dist/services/pg-rest-router.js", + ), + routerSource, + ); + + return root; +} + +async function makeRequest(port, { body, method, path: requestPath }) { + return await new Promise((resolve, reject) => { + const req = http.request( + { + hostname: "127.0.0.1", + port, + path: requestPath, + method, + headers: body + ? { + "content-type": "application/json", + "content-length": Buffer.byteLength(body), + } + : undefined, + }, + (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => { + resolve({ + body: Buffer.concat(chunks).toString("utf8"), + statusCode: res.statusCode, + }); + }); + }, + ); + + req.on("error", reject); + + if (body) { + req.write(body); + } + + req.end(); + }); +} + +async function startServerWithEnv(root) { + const previousEnv = { + TOOLATHLON_GYM_ROOT: process.env.TOOLATHLON_GYM_ROOT, + WOOCOMMERCE_SHIM_HOST: process.env.WOOCOMMERCE_SHIM_HOST, + WOOCOMMERCE_SHIM_PORT: process.env.WOOCOMMERCE_SHIM_PORT, + }; + + const restoreEnv = () => { + for (const [key, value] of Object.entries(previousEnv)) { + if (value === undefined) { + delete process.env[key]; + continue; + } + + process.env[key] = value; + } + }; + + process.env.TOOLATHLON_GYM_ROOT = root; + process.env.WOOCOMMERCE_SHIM_HOST = "127.0.0.1"; + process.env.WOOCOMMERCE_SHIM_PORT = "0"; + + let server; + try { + ({ server } = await serverModule.createWooCommerceShimServer()); + + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", resolve); + }); + + const address = server.address(); + assert.ok(address && typeof address !== "string"); + + return { + port: address.port, + restore() { + restoreEnv(); + }, + server, + }; + } catch (error) { + restoreEnv(); + await new Promise((resolve) => { + server?.close(resolve); + }); + throw error; + } +} + +function withEnv(overrides) { + const previousEnv = {}; + + for (const [key, value] of Object.entries(overrides)) { + previousEnv[key] = process.env[key]; + + if (value === undefined) { + delete process.env[key]; + continue; + } + + process.env[key] = value; + } + + return () => { + for (const [key, value] of Object.entries(previousEnv)) { + if (value === undefined) { + delete process.env[key]; + continue; + } + + process.env[key] = value; + } + }; +} + +test("forwards WooCommerce POST requests to the router", async () => { + const root = createGymRoot(` + export class PgRestRouter { + async post(path, data, options) { + return { + status: 201, + data: { data, options, path }, + }; + } + } + `); + + const runtime = await startServerWithEnv(root); + + try { + const response = await makeRequest(runtime.port, { + method: "POST", + path: "/wp-json/wc/v3/orders?status=pending&status=paid", + body: JSON.stringify({ id: 42 }), + }); + + assert.equal(response.statusCode, 201); + assert.deepEqual(JSON.parse(response.body), { + data: { id: 42 }, + options: { params: { status: ["pending", "paid"] } }, + path: "orders", + }); + } finally { + runtime.restore(); + await new Promise((resolve) => { + runtime.server.close(resolve); + }); + } +}); + +test("rejects malformed WooCommerce JSON payloads with 400", async () => { + const root = createGymRoot(` + export class PgRestRouter { + async post() { + return { + status: 201, + data: { ok: true }, + }; + } + } + `); + + const runtime = await startServerWithEnv(root); + + try { + const response = await makeRequest(runtime.port, { + method: "POST", + path: "/wp-json/wc/v3/orders", + body: "{", + }); + + assert.equal(response.statusCode, 400); + assert.deepEqual(JSON.parse(response.body), { error: "invalid_json" }); + } finally { + runtime.restore(); + await new Promise((resolve) => { + runtime.server.close(resolve); + }); + } +}); + +test("maps downstream SyntaxError failures to opaque 500 responses", async () => { + const root = createGymRoot(` + export class PgRestRouter { + async get() { + throw new SyntaxError("boom: secret detail"); + } + } + `); + + const runtime = await startServerWithEnv(root); + const consoleErrors = []; + const originalConsoleError = console.error; + console.error = (...args) => { + consoleErrors.push(args); + }; + + try { + const response = await makeRequest(runtime.port, { + method: "GET", + path: "/wp-json/wc/v3/orders", + }); + + assert.equal(response.statusCode, 500); + assert.deepEqual(JSON.parse(response.body), { error: "internal_error" }); + assert.equal(consoleErrors.length > 0, true); + assert.match(String(consoleErrors[0][0]), /boom: secret detail/); + } finally { + console.error = originalConsoleError; + runtime.restore(); + await new Promise((resolve) => { + runtime.server.close(resolve); + }); + } +}); + +test("rejects when the shim server fails to bind", async () => { + const root = createGymRoot(` + globalThis.__wooCleanupStats = { endCalls: 0 }; + + export class PgRestRouter { + constructor() { + this.pool = { + end: async () => { + globalThis.__wooCleanupStats.endCalls += 1; + }, + }; + } + } + `); + const restoreEnv = withEnv({ + TOOLATHLON_GYM_ROOT: root, + WOOCOMMERCE_SHIM_HOST: "127.0.0.1", + WOOCOMMERCE_SHIM_PORT: "38082", + }); + const originalCreateServer = http.createServer; + let closed = 0; + + http.createServer = () => { + const server = new EventEmitter(); + server.close = (callback) => { + closed += 1; + callback?.(); + }; + server.listen = () => { + process.nextTick(() => { + if (server.listenerCount("error") > 0) { + const error = new Error("bind failed"); + error.code = "EADDRINUSE"; + server.emit("error", error); + } + }); + return server; + }; + return server; + }; + + try { + const startPromise = Promise.race([ + serverModule.startWooCommerceShimServer(), + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("timed out waiting for start failure")); + }, 200); + }), + ]); + + await assert.rejects(startPromise, { code: "EADDRINUSE" }); + await assert.rejects(startPromise, { code: "EADDRINUSE" }); + } finally { + http.createServer = originalCreateServer; + restoreEnv(); + } + + assert.equal(closed, 1); + assert.equal(globalThis.__wooCleanupStats.endCalls, 1); + delete globalThis.__wooCleanupStats; +}); + +test("does not leak derived PG env across repeated server creation", async () => { + const restoreEnv = withEnv({ + PG_HOST: undefined, + PG_PORT: undefined, + PG_DATABASE: undefined, + PG_USER: undefined, + PG_PASSWORD: undefined, + WOOCOMMERCE_SHIM_HOST: "127.0.0.1", + WOOCOMMERCE_SHIM_PORT: "0", + }); + + try { + const root = createGymRoot(` + export class PgRestRouter { + constructor() { + this.snapshot = { + host: process.env.PG_HOST, + port: process.env.PG_PORT, + database: process.env.PG_DATABASE, + user: process.env.PG_USER, + password: process.env.PG_PASSWORD, + }; + } + } + `); + let firstRuntime; + const restoreFirstEnv = withEnv({ + TOOLATHLON_GYM_ROOT: root, + POSTGRES_HOST_PORT: "15432", + POSTGRES_DB: "first_db", + POSTGRES_USER: "first_user", + POSTGRES_PASSWORD: "first_password", + PG_USER: "explicit_first_user", + }); + try { + firstRuntime = await serverModule.createWooCommerceShimServer(); + } finally { + restoreFirstEnv(); + } + + let secondRuntime; + const restoreSecondEnv = withEnv({ + TOOLATHLON_GYM_ROOT: root, + POSTGRES_HOST_PORT: "25432", + POSTGRES_DB: "second_db", + POSTGRES_USER: "second_user", + POSTGRES_PASSWORD: "second_password", + }); + try { + secondRuntime = await serverModule.createWooCommerceShimServer(); + } finally { + restoreSecondEnv(); + } + + assert.deepEqual(firstRuntime.router.snapshot, { + host: "127.0.0.1", + port: "15432", + database: "first_db", + user: "explicit_first_user", + password: "first_password", + }); + assert.deepEqual(secondRuntime.router.snapshot, { + host: "127.0.0.1", + port: "25432", + database: "second_db", + user: "second_user", + password: "second_password", + }); + assert.equal(process.env.PG_PORT, undefined); + assert.equal(process.env.PG_USER, undefined); + } finally { + restoreEnv(); + } +}); + +test("serves /healthz without touching the router", async () => { + const root = createGymRoot(` + export class PgRestRouter { + async get() { + throw new Error("router should not be called"); + } + } + `); + + const runtime = await startServerWithEnv(root); + + try { + const response = await makeRequest(runtime.port, { + method: "GET", + path: "/healthz", + }); + + assert.equal(response.statusCode, 200); + assert.deepEqual(JSON.parse(response.body), { ok: true }); + } finally { + runtime.restore(); + await new Promise((resolve) => { + runtime.server.close(resolve); + }); + } +}); diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.mjs new file mode 100644 index 00000000..72c0ab3a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.mjs @@ -0,0 +1,9 @@ +const WOO_PREFIX = "/wp-json/wc/v3/"; + +export function stripWooPrefix(pathname) { + if (!pathname.startsWith(WOO_PREFIX)) { + throw new Error(`WooCommerce REST prefix not found: ${pathname}`); + } + + return pathname.slice(WOO_PREFIX.length); +} diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.test.mjs new file mode 100644 index 00000000..44d8fc91 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { stripWooPrefix } from "./strip_prefix.mjs"; + +test("strips the WooCommerce REST prefix from matching paths", () => { + assert.equal( + stripWooPrefix("/wp-json/wc/v3/orders/12"), + "orders/12", + ); +}); + +test("rejects non-matching WooCommerce REST paths", () => { + assert.throws( + () => stripWooPrefix("/api/orders/12"), + /WooCommerce REST prefix/, + ); +}); diff --git a/sandbox/server/backends/resources/mcp/toolathlon_gym.py b/sandbox/server/backends/resources/mcp/toolathlon_gym.py index 9a95baec..a365c589 100644 --- a/sandbox/server/backends/resources/mcp/toolathlon_gym.py +++ b/sandbox/server/backends/resources/mcp/toolathlon_gym.py @@ -22,6 +22,7 @@ from .client import MCPStdioClient, load_mcp_process_config logger = logging.getLogger("MCPBackend") +_BUNDLED_MCP_SERVERS_PATH = Path(__file__).resolve().parent / "vendor" / "local_servers" class ToolathlonGymBackend(Backend): @@ -235,16 +236,34 @@ async def _dispatch( session_id=session_id, ) - def _get_mcp_servers_path(self) -> str | None: - """Return the configured path to MCP server executables, or None.""" + def _get_mcp_servers_path(self) -> str: + """Return the configured path to MCP server executables.""" value = self.get_default_config().get("mcp_servers_path") if value: - return str(value) + explicit_path = Path(value) + if not explicit_path.is_dir(): + raise RuntimeError( + "Configured mcp_servers_path does not exist or is not a directory: " + f"'{explicit_path}'" + ) + return str(explicit_path) # Backward compat: derive from toolathlon_root. toolathlon_root = self.get_default_config().get("toolathlon_root") if toolathlon_root: - return str(Path(toolathlon_root) / "local_servers") - return None + derived_path = Path(toolathlon_root) / "local_servers" + if not derived_path.is_dir(): + raise RuntimeError( + "Configured toolathlon_root resolves to a missing or invalid " + f"local_servers directory: '{derived_path}'" + ) + return str(derived_path) + if _BUNDLED_MCP_SERVERS_PATH.exists(): + return str(_BUNDLED_MCP_SERVERS_PATH) + raise RuntimeError( + "Bundled MCP servers are missing at " + f"'{_BUNDLED_MCP_SERVERS_PATH}'. Vendor them into the package or " + "configure an explicit mcp_servers_path." + ) def _get_workspace_root(self) -> Path: value = self.get_default_config().get("workspace_root") or "/tmp/agentflow_mcp" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.github/ISSUE_TEMPLATE/bug-report.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.github/ISSUE_TEMPLATE/bug-report.yaml new file mode 100644 index 00000000..f05398a3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -0,0 +1,30 @@ +name: Bug反馈 +description: 反馈使用遇到的问题。 +title: "[BUG] 请在标题中简短描述问题,便于查看。" +labels: bug + +body: + - type: textarea + id: bug-description + attributes: + label: BUG描述 + description: 尽量详细描述BUG问题。A clear and concise description of what the bug is. + placeholder: 尽量详细描述BUG问题。A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshot截图 + description: 尽量提供运行时报错或者结果错误的参数截图,包括调用参数。Please try to provide screenshots of the parameters that cause runtime errors or incorrect results, including the invocation parameters. + placeholder: 尽量提供运行时报错或者结果错误的参数截图,包括调用参数。Please try to provide screenshots of the parameters that cause runtime errors or incorrect results, including the invocation parameters. + validations: + required: true + - type: textarea + id: addtional-context + attributes: + label: Additional context其他信息 + description: 其他信息。Add any other context about the problem here. + placeholder: 其他信息。Add any other context about the problem here. + validations: + required: true diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.gitignore new file mode 100644 index 00000000..1170717c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.gitignore @@ -0,0 +1,136 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.prettierrc b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.prettierrc new file mode 100644 index 00000000..35895267 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "useTabs": false, + "trailingComma": "es5", + "printWidth": 80, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "auto" + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/Dockerfile new file mode 100644 index 00000000..8a26789e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/Dockerfile @@ -0,0 +1,3 @@ +FROM node:lts-alpine +WORKDIR /mcp +RUN npm install 12306-mcp \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/LICENSE new file mode 100644 index 00000000..f95efca1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Jok + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/README.md new file mode 100644 index 00000000..77fc9a50 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/README.md @@ -0,0 +1,116 @@ +#
12306-mcp
+ +
+ +[![](https://img.shields.io/badge/Joooook-blue.svg?logo=github&lable=python&labelColor=497568&color=497568&style=flat-square)](https://github.com/Joooook) +[![](https://img.shields.io/badge/Joooook-blue.svg?logo=bilibili&logoColor=white&lable=python&labelColor=af7a82&color=af7a82&style=flat-square)](https://space.bilibili.com/3546386788255839) +![](https://img.shields.io/badge/typescript-blue.svg?logo=typescript&lable=typescript&logoColor=white&labelColor=192c3b&color=192c3b&style=flat-square) +![](https://img.shields.io/github/stars/Joooook/12306-mcp?logo=reverbnation&lable=python&logoColor=white&labelColor=ffc773&color=ffc773&style=flat-square) +![](https://img.shields.io/github/last-commit/Joooook/12306-mcp.svg?style=flat-square) +![](https://img.shields.io/github/license/Joooook/12306-mcp.svg?style=flat-square&color=000000) +
+ +A 12306 ticket search server based on the Model Context Protocol (MCP). The server provides a simple API interface that allows users to search for 12306 tickets. + +基于 Model Context Protocol (MCP) 的12306购票搜索服务器。提供了简单的API接口,允许大模型利用接口搜索12306购票信息。 + +##
🚩Features
+
+ +| 功能描述 | 状态 | +|------------------------------|--------| +| 查询12306购票信息 | ✅ 已完成 | +| 过滤列车信息 | ✅ 已完成 | +| 过站查询 | ✅ 已完成 | +| 中转查询 | ✅ 已完成 | +| 其余接口,欢迎提feature | 🚧 计划内 | + +
+
+ +
+
+ +
+ +##
⚙️Installation
+ +~~~bash +git clone https://github.com/Joooook/12306-mcp.git +npm i +~~~ + + +##
▶️Quick Start
+ +### CLI-stdio +~~~bash +npx -y 12306-mcp +~~~ + +### CLI-http +~~~bash +npx -y 12306-mcp --port [端口号] +~~~ + +### MCP sever configuration + +~~~json +{ + "mcpServers": { + "12306-mcp": { + "command": "npx", + "args": [ + "-y", + "12306-mcp" + ] + } + } +} + +~~~ + +### Docker-stdio +~~~bash +docker build . -t 12306-mcp +docker run --rm -it 12306-mcp npx 12306-mcp +~~~ + +### Docker-http +~~~bash +docker build . -t 12306-mcp +docker run -p [your_port]:8080 -d 12306-mcp npx 12306-mcp --port 8080 +~~~ + + + +##
📚Documentation
+ +- [服务原理详解](./docs/principle.md) 12306-MCP服务的工作原理 +- [架构图](./docs/architecture.md) 12306-MCP服务的架构图 + ![12306-MCP 服务架构图](./docs/architecture.png) + +##
👉️Reference
+- [modelcontextprotocol/modelcontextprotocol](https://github.com/modelcontextprotocol/modelcontextprotocol) +- [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) + +##
💭Murmurs
+本项目仅用于学习,欢迎催更。 + +##
🎫Badges
+
+ + 12306-mcp MCP server + + +[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/joooook-12306-mcp-badge.png)](https://mseep.ai/app/joooook-12306-mcp) + +
+ +##
☕️Donate
+请我喝杯奶茶吧。 +
+ + + +
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.md new file mode 100644 index 00000000..42da4c4e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.md @@ -0,0 +1,67 @@ +# 12306-MCP 服务架构 + +## 整体架构 + +![12306-MCP 服务架构图](./architecture.png) + + +```mermaid +graph TB + subgraph "用户层" + User["用户"] + LLM["大语言模型"] + end + + subgraph "MCP 服务层" + McpServer["MCP Server"] + + subgraph "基础工具层" + GetDate["get-current-date"] + GetCityStations["get-stations-code-in-city"] + GetCityCodes["get-station-code-of-citys"] + GetStationNames["get-station-code-by-names"] + GetStationInfo["get-station-by-telecode"] + end + + subgraph "核心工具层" + GetTickets["get-tickets"] + GetInterline["get-interline-tickets"] + GetRoute["get-train-route-stations"] + end + end + + subgraph "数据层" + Stations["STATIONS
(车站id→车站信息)"] + CityStationsMap["CITY_STATIONS
(城市名→车站id(可能一个城市多个站))"] + CityCodes["CITY_CODES
(车站名(与城市名相同,只会有一个) →车站id)"] + NameStations["NAME_STATIONS
(车站名→车站id)"] + end + + subgraph "外部服务" + Api12306["12306 API"] + end + + User --> LLM + LLM <--> McpServer + + GetDate -.-> GetTickets + GetDate -.-> GetInterline + GetDate -.-> GetRoute + GetCityCodes -.-> GetTickets + GetCityCodes -.-> GetInterline + GetStationNames -.-> GetTickets + GetStationNames -.-> GetInterline + GetStationNames -.-> GetRoute + GetTickets -.-> GetRoute + + + GetTickets --> Api12306 + GetInterline --> Api12306 + GetRoute --> Api12306 + + GetCityStations --> CityStationsMap + GetCityCodes --> CityCodes + GetStationNames --> NameStations + GetStationInfo --> Stations +``` + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.png new file mode 100644 index 00000000..bb172276 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/principle.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/principle.md new file mode 100644 index 00000000..71aa6db5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/principle.md @@ -0,0 +1,164 @@ +# 12306-MCP 服务 原理 + +## 1. 启动初始化 + +### 1.1 车站数据加载 + +服务启动时通过 `getStations()` 函数从 12306 API 获取全国车站信息,构建四个核心索引: + +**具体流程:** +1. 访问 12306 首页 (https://www.12306.cn/index/) +2. 从 HTML 中提取车站名称 JS 文件路径 +3. 下载并解析 JS 文件,获取原始车站数据 +4. 补充缺失的车站信息 (MISSING_STATIONS) +5. 基于车站数据得到四个核心数据结构映射表 + +```typescript +// 1. 车站id -> 车站信息 +STATIONS: Record + +// "AAA": { +// "station_id": "@aaa", +// "station_name": "北京北", +// "station_code": "AAA", +// "station_pinyin": "beijingbei", +// "station_short": "aaa", +// "station_index": "0", +// "code": "1234", +// "city": "北京", +// "r1": "", +// "r2": "" +// } + + +// 2. 城市名 -> 车站id 和 站名 (可能一个城市多个站) +CITY_STATIONS: Record + +// "北京": [{"station_code": "AAA","station_name": "北京北"},{"station_code": "BBB","station_name": "京东"},...] + +// 3. 车站名(与城市名相同,只会有一个) -> 车站id 和 站名 +CITY_CODES: Record + +// "北京":{"station_code":"CCC","station_name":"北京"} + +// 4. 车站名 -> 车站id 和 站名 +NAME_STATIONS: Record + +// "北京北":{"station_code":"AAA","station_name":"北京北"} + +``` + +## 2. MCP tools + +### 2.1 基础tool + +- **`get-current-date`**: 获取上海时区当前日期 + - 返回当前上海时区的时间日期字符串("yyyy-MM-dd") + - 为其他工具提供准确的查询日期基准 + +- **`get-stations-code-in-city`**: 查询城市内所有车站(使用 `CITY_STATIONS`) + - 输入:中文城市名 + - 查找:`CITY_STATIONS[city]` 获取该城市所有车站列表 + - 返回:包含 `station_code` 和 `station_name` 的数组 + +- **`get-station-code-of-citys`**: 获取城市代表车站id(使用 `CITY_CODES`) + - 输入:城市名(支持 "|" 分隔的多个城市) + - 查找:`CITY_CODES[city]` 获取与城市同名的主要车站 + - 返回:每个城市对应的代表车站信息 + +- **`get-station-code-by-names`**: 车站名转车站id(使用 `NAME_STATIONS`) + - 输入:具体车站名(支持 "|" 分隔的多个车站) + - 查找:`NAME_STATIONS[stationName]` 精确匹配车站名 + - 返回:车站id和车站名 + +- **`get-station-by-telecode`**: 车站id查车站信息(使用 `STATIONS`) + - 输入:车站id + - 查找:`STATIONS[telecode]` 获取完整车站信息 + - 返回:包含拼音、城市等详细信息 + +### 2.2 核心tool (输入可通过基础tool获取) + +- **`get-tickets`**: 查询12306余票信息 + - 输入:出发日期、出发站id、到达站id、车次类型筛选 + - 参数处理:检查日期不早于当前日期,验证车站id存在性, 构造请求入参 + - Cookie 处理:先获取 12306 Cookie 用于身份验证 + - API 调用:访问 `/otn/leftTicket/query` 接口 + - 数据处理, 车次类型筛选 + - 返回格式化数据 + +- **`get-interline-tickets`**: 中转换乘查询,支持指定中转站 + - 输入:出发站id、到达站id、中转站id、是否显示无座、车次类型筛选 + - 参数处理:检查日期不早于当前日期,验证车站id存在性, 构造请求入参 + - Cookie 处理:先获取 12306 Cookie 用于身份验证 + - API 调用:访问 `/lcquery/queryU` 接口 + - 数据处理, 车次类型筛选 + - 返回格式化数据 + +- **`get-train-route-stations`**: 列车经停站查询 + - 输入:车次编码(可以调用get-tickets获取)、出发站id、到达站id、出发日期 + - 参数处理:检查日期不早于当前日期,验证车站id存在性, 构造请求入参 + - Cookie 处理:先获取 12306 Cookie 用于身份验证 + - API 调用:访问 `/otn/czxx/queryByTrainNo` 接口 + - 返回格式化数据 + +## 3. 数据流程与工具关系 + +### 3.1 车票查询流程 + +``` +用户查询 "后天北京到上海的高铁" - 大模型调用流程: + ↓ +1. get-current-date() → "2024-01-15" (获取当前日期) +2. 大模型理解后天日期 → "2024-01-17" +3. get-station-code-of-citys("北京|上海") → {"北京": {"station_code": "BJP","station_name": "北京"}, "上海": {"station_code": "SHH","station_name": "上海"}} + ↓ +4. get-tickets(date: "2024-01-17", fromStation: "BJP", toStation: "SHH", trainFilterFlags: "G") + ↓ +5. 内部数据处理(参数验证, Cookie获取, API调用, 格式化输出文本) + ↓ +6. 返回格式化的高铁车次信息(车次、时间、价格、余票等) +``` + +### 3.2 中转查询流程 + +``` +用户查询 "深圳到拉萨,经过西安中转" + ↓ +1. 获取三个城市的车站id +2. get-interline-tickets(from: "SZQ", to: "LSO", transfer: "XAY") + ↓ +3. 内部数据处理(参数验证, Cookie获取, API调用, 格式化输出文本) + ↓ +4. 返回中转方案(第一程 + 第二程) +``` + +### 3.3 经停站查询流程 + +``` +用户查询 "G1次列车经停哪些站" + ↓ +1. get-train-route-stations(trainNo: "G1", from: "BJP", to: "SHH") + ↓ +2. 数据处理:parseRouteStationsData() → parseRouteStationsInfo() + ↓ +3. 返回经停站列表(站名、到达时间、出发时间、停留时间) +``` + +### 3.4 工具依赖关系 + +``` +基础工具层: +├── get-current-date +├── get-stations-code-in-city +└── get-station-code-of-citys +└── get-station-code-by-names +└── get-station-by-telecode + + ↓ 为核心工具层提供基础数据 + +核心工具层: +├── get-tickets (依赖车站id) +├── get-interline-tickets (依赖车站id) +└── get-train-route-stations (依赖车站id和车次号) +``` + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/glama.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/glama.json new file mode 100644 index 00000000..e88e8ede --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/glama.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://glama.ai/mcp/schemas/server.json", + "maintainers": [ + "Joooook" + ] +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package-lock.json new file mode 100644 index 00000000..0c8add91 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package-lock.json @@ -0,0 +1,3301 @@ +{ + "name": "12306-mcp", + "version": "0.3.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "12306-mcp", + "version": "0.3.5", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/inspector": "^0.16.5", + "@modelcontextprotocol/sdk": "^1.9.0", + "@types/pg": "^8.18.0", + "axios": "^1.8.4", + "commander": "^14.0.0", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "mcp-http-server": "^1.1.5", + "pg": "^8.20.0", + "zod": "^3.24.2" + }, + "bin": { + "12306-mcp": "build/index.js" + }, + "devDependencies": { + "@types/node": "^22.14.1", + "run-script-os": "^1.1.6", + "typescript": "^5.8.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@modelcontextprotocol/inspector": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector/-/inspector-0.16.5.tgz", + "integrity": "sha512-3viCtcLn1p6ZYZG8L9DxnNx2XUw6damCZM37nBByKAd74stH7nZa2VxFAwLpUCjKylYqawCRthzYSyeSlWGf1g==", + "license": "MIT", + "workspaces": [ + "client", + "server", + "cli" + ], + "dependencies": { + "@modelcontextprotocol/inspector-cli": "^0.16.5", + "@modelcontextprotocol/inspector-client": "^0.16.5", + "@modelcontextprotocol/inspector-server": "^0.16.5", + "@modelcontextprotocol/sdk": "^1.17.3", + "concurrently": "^9.2.0", + "open": "^10.2.0", + "shell-quote": "^1.8.3", + "spawn-rx": "^5.1.2", + "ts-node": "^10.9.2", + "zod": "^3.25.76" + }, + "bin": { + "mcp-inspector": "cli/build/cli.js" + }, + "engines": { + "node": ">=22.7.5" + } + }, + "node_modules/@modelcontextprotocol/inspector-cli": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-cli/-/inspector-cli-0.16.5.tgz", + "integrity": "sha512-6Flp9goLJutjUZTx6clDo4x/6TA7BwqeTGSYcC8ZeP8IEKjx+EZpvpYPVAV++rkHJYa0F5PViy02CMoGBGkzkg==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.3", + "commander": "^13.1.0", + "spawn-rx": "^5.1.2" + }, + "bin": { + "mcp-inspector-cli": "build/cli.js" + } + }, + "node_modules/@modelcontextprotocol/inspector-cli/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/inspector-client": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-client/-/inspector-client-0.16.5.tgz", + "integrity": "sha512-KjgtTRdFSDt964a9KtmF3aRpi4ntd+6wn3e4WUa5vcK7H8f34rq4wfIGF22dd/xoKbALP3zVVAsPqVN94SCGmg==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.3", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.3", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-tooltip": "^1.1.8", + "ajv": "^6.12.6", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.4", + "lucide-react": "^0.523.0", + "pkce-challenge": "^4.1.0", + "prismjs": "^1.30.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-simple-code-editor": "^0.14.1", + "serve-handler": "^6.1.6", + "tailwind-merge": "^2.5.3", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.25.76" + }, + "bin": { + "mcp-inspector-client": "bin/start.js" + } + }, + "node_modules/@modelcontextprotocol/inspector-client/node_modules/pkce-challenge": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", + "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@modelcontextprotocol/inspector-server": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-server/-/inspector-server-0.16.5.tgz", + "integrity": "sha512-mWKrEpimfNdSFOxMJGj3cYN1PxHANW9vjpek+tR0TLiP7ogq7DLV4bbsa+lPPGwOVogUIyUiXbffY8M1YHRZVA==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.3", + "cors": "^2.8.5", + "express": "^5.1.0", + "ws": "^8.18.0", + "zod": "^3.25.76" + }, + "bin": { + "mcp-inspector-server": "build/index.js" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", + "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.523.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.523.0.tgz", + "integrity": "sha512-rUjQoy7egZT9XYVXBK1je9ckBnNp7qzRZOhLQx5RcEp2dCGlXo+mv6vf7Am4LimEcFBJIIZzSGfgTqc9QCrPSw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mcp-http-server": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/mcp-http-server/-/mcp-http-server-1.1.5.tgz", + "integrity": "sha512-bmKEo88wtJ88GSHrBd6zmjM19HU00c4FZ5YRt61nhHzpQjiryvRwmhNsKvbDwapBq94F7EePH9BJehBjXh17Bg==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.11.2", + "express": "^5.1.0" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-simple-code-editor": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz", + "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-script-os": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz", + "integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==", + "dev": true, + "bin": { + "run-os": "index.js", + "run-script-os": "index.js" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-handler/node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/serve-handler/node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spawn-rx": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-5.1.2.tgz", + "integrity": "sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7", + "rxjs": "^7.8.1" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "license": "MIT", + "peer": true + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package.json new file mode 100644 index 00000000..00d45448 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package.json @@ -0,0 +1,50 @@ +{ + "name": "12306-mcp", + "version": "0.3.5", + "main": "build/index.js", + "scripts": { + "prebuild": "run-script-os", + "prebuild:win32": "del /q /s build\\* >nul 2>&1", + "prebuild:default": "rm -rf build/*", + "build": "tsc", + "postbuild": "run-script-os", + "postbuild:darwin:linux": "chmod +x build/index.js", + "postbuild:default": "", + "prepare": "npm run build", + "test": "tsc && node ./build/index.js", + "test:http": "tsc && node ./build/index.js --port 8080", + "debug": "tsc && npx @modelcontextprotocol/inspector node ./build/index.js" + }, + "keywords": [ + "mcp", + "12306", + "mcp-server" + ], + "author": "joooook", + "license": "MIT", + "description": "This is a 12306 ticket search server based on the Model Context Protocol (MCP). ", + "dependencies": { + "@modelcontextprotocol/inspector": "^0.16.5", + "@modelcontextprotocol/sdk": "^1.9.0", + "@types/pg": "^8.18.0", + "axios": "^1.8.4", + "commander": "^14.0.0", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "mcp-http-server": "^1.1.5", + "pg": "^8.20.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.14.1", + "run-script-os": "^1.1.6", + "typescript": "^5.8.3" + }, + "type": "module", + "bin": { + "12306-mcp": "./build/index.js" + }, + "files": [ + "build" + ] +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/index.ts new file mode 100644 index 00000000..70905a4c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/index.ts @@ -0,0 +1,432 @@ +#!/usr/bin/env node + +// PostgreSQL-backed 12306 MCP server +// All API calls to kyfw.12306.cn are replaced with local PostgreSQL queries. + +import { program } from 'commander'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { format } from 'date-fns'; +import { toZonedTime } from 'date-fns-tz'; +import pg from 'pg'; +import { + StationData, + StationDataKeys, + TicketInfo, + RouteStationInfo, + Price, +} from './types.js'; + +const { Pool } = pg; + +const VERSION = '0.3.5-pg'; + +// PostgreSQL pool +const pool = new Pool({ + host: process.env.PG_HOST || 'localhost', + port: parseInt(process.env.PG_PORT || '5432'), + database: process.env.PG_DATABASE || 'toolathlon', + user: process.env.PG_USER || 'postgres', + password: process.env.PG_PASSWORD || 'postgres', + idleTimeoutMillis: 10000, +}); +pool.on('error', () => { /* swallow idle connection errors */ }); + +// --------------------------------------------------------------------------- +// Station dictionaries (loaded from PostgreSQL at startup) +// --------------------------------------------------------------------------- +let STATIONS: Record = {}; +let CITY_STATIONS: Record = {}; +let CITY_CODES: Record = {}; +let NAME_STATIONS: Record = {}; + +async function loadStations(): Promise { + const result = await pool.query( + `SELECT station_code, station_name, station_pinyin, station_short, city, + station_id, station_index, code, r1, r2 + FROM train.stations` + ); + STATIONS = {}; + CITY_STATIONS = {}; + CITY_CODES = {}; + NAME_STATIONS = {}; + for (const row of result.rows) { + const station = row as StationData; + STATIONS[station.station_code] = station; + // CITY_STATIONS + if (!CITY_STATIONS[station.city]) CITY_STATIONS[station.city] = []; + CITY_STATIONS[station.city].push({ station_code: station.station_code, station_name: station.station_name }); + // CITY_CODES: prefer station whose name == city + if (station.station_name === station.city || !CITY_CODES[station.city]) { + CITY_CODES[station.city] = { station_code: station.station_code, station_name: station.station_name }; + } + // NAME_STATIONS + NAME_STATIONS[station.station_name] = { station_code: station.station_code, station_name: station.station_name }; + } +} + +// --------------------------------------------------------------------------- +// Seat type constants (kept from original) +// --------------------------------------------------------------------------- +const SEAT_TYPES: Record = { + '9': { name: '商务座', short: 'swz' }, + P: { name: '特等座', short: 'tz' }, + M: { name: '一等座', short: 'zy' }, + O: { name: '二等座', short: 'ze' }, + '6': { name: '高级软卧', short: 'gr' }, + '4': { name: '软卧', short: 'rw' }, + F: { name: '动卧', short: 'rw' }, + '3': { name: '硬卧', short: 'yw' }, + '2': { name: '软座', short: 'rz' }, + '1': { name: '硬座', short: 'yz' }, + W: { name: '无座', short: 'wz' }, + H: { name: '其他', short: 'qt' }, +}; + +function formatTicketStatus(num: string): string { + if (num.match(/^\d+$/)) { + const count = parseInt(num); + return count === 0 ? '无票' : `剩余${count}张票`; + } + switch (num) { + case '有': case '充足': return '有票'; + case '无': case '--': case '': return '无票'; + case '候补': return '无票需候补'; + default: return `${num}票`; + } +} + +function formatTicketsInfo(ticketsInfo: TicketInfo[]): string { + if (ticketsInfo.length === 0) return '没有查询到相关车次信息'; + let result = '车次 | 出发站 -> 到达站 | 出发时间 -> 到达时间 | 历时\n'; + for (const ti of ticketsInfo) { + let s = `${ti.start_train_code}(实际车次train_no: ${ti.train_no}) ${ti.from_station}(telecode: ${ti.from_station_telecode}) -> ${ti.to_station}(telecode: ${ti.to_station_telecode}) ${ti.start_time} -> ${ti.arrive_time} 历时:${ti.lishi}`; + for (const price of ti.prices) { + s += `\n- ${price.seat_name}: ${formatTicketStatus(price.num)} ${price.price}元`; + } + result += `${s}\n`; + } + return result; +} + +function formatTicketsInfoCSV(ticketsInfo: TicketInfo[]): string { + if (ticketsInfo.length === 0) return '没有查询到相关车次信息'; + let result = '车次,实际车次train_no,出发站,到达站,出发时间,到达时间,历时,票价信息,特色标签\n'; + for (const ti of ticketsInfo) { + let priceStr = '['; + for (const p of ti.prices) priceStr += `${p.seat_name}: ${formatTicketStatus(p.num)}${p.price}元,`; + priceStr += ']'; + result += `${ti.start_train_code},${ti.train_no},${ti.from_station}(telecode:${ti.from_station_telecode}),${ti.to_station}(telecode:${ti.to_station_telecode}),${ti.start_time},${ti.arrive_time},${ti.lishi},${priceStr},${ti.dw_flag.join('&') || '/'}\n`; + } + return result; +} + +const TIME_COMPARATORS: Record number> = { + startTime: (a, b) => { + const [ah, am] = a.start_time.split(':').map(Number); + const [bh, bm] = b.start_time.split(':').map(Number); + return ah * 60 + am - (bh * 60 + bm); + }, + arriveTime: (a, b) => { + const [ah, am] = a.arrive_time.split(':').map(Number); + const [bh, bm] = b.arrive_time.split(':').map(Number); + return ah * 60 + am - (bh * 60 + bm); + }, + duration: (a, b) => { + const [ah, am] = a.lishi.split(':').map(Number); + const [bh, bm] = b.lishi.split(':').map(Number); + return ah * 60 + am - (bh * 60 + bm); + }, +}; + +function filterTicketsInfo( + tickets: TicketInfo[], + trainFilterFlags: string, + earliestStartTime = 0, + latestStartTime = 24, + sortFlag = '', + sortReverse = false, + limitedNum = 0 +): TicketInfo[] { + let result = trainFilterFlags + ? tickets.filter(t => { + for (const flag of trainFilterFlags) { + if (flag === 'G' && (t.start_train_code.startsWith('G') || t.start_train_code.startsWith('C'))) return true; + if (flag === 'D' && t.start_train_code.startsWith('D')) return true; + if (flag === 'Z' && t.start_train_code.startsWith('Z')) return true; + if (flag === 'T' && t.start_train_code.startsWith('T')) return true; + if (flag === 'K' && t.start_train_code.startsWith('K')) return true; + } + return false; + }) + : tickets; + result = result.filter(t => { + const h = parseInt(t.start_time.split(':')[0], 10); + return h >= earliestStartTime && h < latestStartTime; + }); + if (sortFlag && TIME_COMPARATORS[sortFlag]) { + result.sort(TIME_COMPARATORS[sortFlag]); + if (sortReverse) result.reverse(); + } + return limitedNum > 0 ? result.slice(0, limitedNum) : result; +} + +function checkDate(date: string): boolean { + return true; // date check disabled — mock data uses historical dates +} + +// --------------------------------------------------------------------------- +// Query tickets from PostgreSQL +// --------------------------------------------------------------------------- +async function queryTickets(date: string, fromStation: string, toStation: string): Promise { + const result = await pool.query( + `SELECT t.id, t.train_no, t.station_train_code, t.from_station_telecode, t.to_station_telecode, + t.start_time, t.arrive_time, t.lishi, t.dw_flags, + sf.station_name AS from_name, st.station_name AS to_name + FROM train.trains t + JOIN train.stations sf ON sf.station_code = t.from_station_telecode + JOIN train.stations st ON st.station_code = t.to_station_telecode + WHERE t.from_station_telecode = $1 + AND t.to_station_telecode = $2 + AND t.depart_date = $3`, + [fromStation, toStation, date] + ); + + const tickets: TicketInfo[] = []; + for (const row of result.rows) { + const seatsResult = await pool.query( + `SELECT seat_type_code, seat_name, seat_short, num, price, discount + FROM train.train_seats WHERE train_id = $1`, + [row.id] + ); + const prices: Price[] = seatsResult.rows.map(s => ({ + seat_name: s.seat_name, + short: s.seat_short, + seat_type_code: s.seat_type_code, + num: s.num, + price: parseFloat(s.price), + discount: s.discount, + })); + tickets.push({ + train_no: row.train_no, + start_train_code: row.station_train_code, + start_date: date, + arrive_date: date, + start_time: row.start_time, + arrive_time: row.arrive_time, + lishi: row.lishi, + from_station: row.from_name, + to_station: row.to_name, + from_station_telecode: row.from_station_telecode, + to_station_telecode: row.to_station_telecode, + prices, + dw_flag: row.dw_flags ? row.dw_flags.split('#').filter((f: string) => f && f !== '0') : [], + }); + } + return tickets; +} + +// --------------------------------------------------------------------------- +// MCP Server +// --------------------------------------------------------------------------- +export const server = new McpServer({ + name: '12306-mcp', + version: VERSION, + capabilities: { resources: {}, tools: {} }, + instructions: '该服务用于查询中国铁路火车票信息(本地模拟数据)。', +}); + +server.resource('stations', 'data://all-stations', async (uri) => ({ + contents: [{ uri: uri.href, text: JSON.stringify(STATIONS) }], +})); + +server.tool( + 'get-current-date', + '获取当前日期,以上海时区(Asia/Shanghai, UTC+8)为准,返回格式为 "yyyy-MM-dd"。主要用于解析用户提到的相对日期(如“明天”、“下周三”),为其他需要日期的接口提供准确的日期输入。', + {}, + async () => { + const nowInShanghai = toZonedTime(new Date(), 'Asia/Shanghai'); + return { content: [{ type: 'text', text: format(nowInShanghai, 'yyyy-MM-dd') }] }; + } +); + +server.tool( + 'get-stations-code-in-city', + '通过中文城市名查询该城市 **所有** 火车站的名称及其对应的 `station_code`,结果是一个包含多个车站信息的列表。', + { city: z.string().describe('中文城市名称,例如:"北京", "上海"') }, + async ({ city }) => { + if (!(city in CITY_STATIONS)) return { content: [{ type: 'text', text: 'Error: City not found.' }] }; + return { content: [{ type: 'text', text: JSON.stringify(CITY_STATIONS[city]) }] }; + } +); + +server.tool( + 'get-station-code-of-citys', + '通过中文城市名查询代表该城市的 `station_code`。此接口主要用于在用户提供**城市名**作为出发地或到达地时,为接口准备 `station_code` 参数。', + { citys: z.string().describe('要查询的城市,比如"北京"。若要查询多个城市,请用|分割,比如"北京|上海"。') }, + async ({ citys }) => { + const result: Record = {}; + for (const city of citys.split('|')) { + result[city] = city in CITY_CODES ? CITY_CODES[city] : { error: '未检索到城市。' }; + } + return { content: [{ type: 'text', text: JSON.stringify(result) }] }; + } +); + +server.tool( + 'get-station-code-by-names', + '通过具体的中文车站名查询其 `station_code` 和车站名。此接口主要用于在用户提供**具体车站名**作为出发地或到达地时,为接口准备 `station_code` 参数。', + { stationNames: z.string().describe('具体的中文车站名称,例如:"北京南", "上海虹桥"。若要查询多个站点,请用|分割,比如"北京南|上海虹桥"。') }, + async ({ stationNames }) => { + const result: Record = {}; + for (const name of stationNames.split('|')) { + result[name] = name in NAME_STATIONS ? NAME_STATIONS[name] : { error: '未检索到车站。' }; + } + return { content: [{ type: 'text', text: JSON.stringify(result) }] }; + } +); + +server.tool( + 'get-station-by-telecode', + '通过车站的 `station_telecode` 查询车站的详细信息,包括名称、拼音、所属城市等。此接口主要用于在已知 `telecode` 的情况下获取更完整的车站数据,或用于特殊查询及调试目的。一般用户对话流程中较少直接触发。', + { stationTelecode: z.string().describe('车站的 `station_telecode` (3位字母编码)') }, + async ({ stationTelecode }) => { + if (!STATIONS[stationTelecode]) return { content: [{ type: 'text', text: 'Error: Station not found.' }] }; + return { content: [{ type: 'text', text: JSON.stringify(STATIONS[stationTelecode]) }] }; + } +); + +server.tool( + 'get-tickets', + '查询12306余票信息。', + { + date: z.string().length(10).describe('查询日期,格式为 "yyyy-MM-dd"。如果用户提供的是相对日期(如“明天”),请务必先调用 `get-current-date` 接口获取当前日期,并计算出目标日期。'), + fromStation: z.string().describe('出发地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'), + toStation: z.string().describe('到达地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'), + trainFilterFlags: z.string().regex(/^[GDZTKOFS]*$/).max(8).optional().default('').describe('车次筛选条件,默认为空,即不筛选。支持多个标志同时筛选。例如用户说“高铁票”,则应使用 "G"。可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]'), + earliestStartTime: z.number().min(0).max(24).optional().default(0).describe('最早出发时间(0-24),默认为0。'), + latestStartTime: z.number().min(0).max(24).optional().default(24).describe('最迟出发时间(0-24),默认为24。'), + sortFlag: z.string().optional().default('').describe('排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]'), + sortReverse: z.boolean().optional().default(false).describe('是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。'), + limitedNum: z.number().min(0).optional().default(0).describe('返回的余票数量限制,默认为0,即不限制。'), + csvFormat: z.boolean().default(false).optional().describe('是否使用CSV格式返回。'), + }, + async ({ date, fromStation, toStation, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum, csvFormat }) => { + if (!checkDate(date)) return { content: [{ type: 'text', text: 'Error: The date cannot be earlier than today.' }] }; + if (!STATIONS[fromStation] || !STATIONS[toStation]) return { content: [{ type: 'text', text: 'Error: Station not found.' }] }; + const tickets = await queryTickets(date, fromStation, toStation); + const filtered = filterTicketsInfo(tickets, trainFilterFlags || '', earliestStartTime, latestStartTime, sortFlag || '', sortReverse, limitedNum); + const text = csvFormat ? formatTicketsInfoCSV(filtered) : formatTicketsInfo(filtered); + return { content: [{ type: 'text', text }] }; + } +); + +server.tool( + 'get-interline-tickets', + '查询12306中转余票信息。尚且只支持查询前十条。', + { + date: z.string().length(10).describe('查询日期,格式为 "yyyy-MM-dd"。如果用户提供的是相对日期(如“明天”),请务必先调用 `get-current-date` 接口获取当前日期,并计算出目标日期。'), + fromStation: z.string().describe('出发地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'), + toStation: z.string().describe('出发地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'), + middleStation: z.string().optional().default('').describe('中转地的 `station_code` ,可选。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'), + showWZ: z.boolean().optional().default(false).describe('是否显示无座车,默认不显示无座车。'), + trainFilterFlags: z.string().regex(/^[GDZTKOFS]*$/).max(8).optional().default('').describe('车次筛选条件,默认为空。从以下标志中选取多个条件组合[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]'), + earliestStartTime: z.number().min(0).max(24).optional().default(0).describe('最早出发时间(0-24),默认为0。'), + latestStartTime: z.number().min(0).max(24).optional().default(24).describe('最迟出发时间(0-24),默认为24。'), + sortFlag: z.string().optional().default('').describe('排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]'), + sortReverse: z.boolean().optional().default(false).describe('是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。'), + limitedNum: z.number().min(1).optional().default(10).describe('返回的中转余票数量限制,默认为10。'), + }, + async ({ date, fromStation, toStation, middleStation, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum }) => { + if (!checkDate(date)) return { content: [{ type: 'text', text: 'Error: The date cannot be earlier than today.' }] }; + + // Find all possible middle stations that have trains from fromStation and to toStation + const midResult = await pool.query( + `SELECT DISTINCT t1.to_station_telecode AS mid + FROM train.trains t1 + JOIN train.trains t2 ON t1.to_station_telecode = t2.from_station_telecode + WHERE t1.from_station_telecode = $1 + AND t2.to_station_telecode = $2 + AND t1.depart_date = $3 AND t2.depart_date = $3 + ${middleStation ? 'AND t1.to_station_telecode = $4' : ''} + LIMIT 5`, + middleStation ? [fromStation, toStation, date, middleStation] : [fromStation, toStation, date] + ); + + if (midResult.rows.length === 0) { + return { content: [{ type: 'text', text: '很抱歉,未查到相关的中转余票信息。' }] }; + } + + let output = '出发时间 -> 到达时间 | 出发车站 -> 中转车站 -> 到达车站 | 换乘标志 |换乘等待时间| 总历时\n\n'; + let count = 0; + for (const { mid } of midResult.rows) { + if (count >= limitedNum) break; + const leg1 = await queryTickets(date, fromStation, mid); + const leg2 = await queryTickets(date, mid, toStation); + const midStation = STATIONS[mid]; + const midName = midStation?.station_name || mid; + for (const t1 of leg1.slice(0, 3)) { + for (const t2 of leg2.slice(0, 3)) { + if (count >= limitedNum) break; + const fromName = STATIONS[fromStation]?.station_name || fromStation; + const toName = STATIONS[toStation]?.station_name || toStation; + output += `${date} ${t1.start_time} -> ${date} ${t2.arrive_time} | `; + output += `${fromName} -> ${midName} -> ${toName} | 同站换乘 | - | -\n\n`; + output += '\t第一段: ' + t1.start_train_code + ' ' + t1.start_time + ' -> ' + t1.arrive_time + '\n'; + output += '\t第二段: ' + t2.start_train_code + ' ' + t2.start_time + ' -> ' + t2.arrive_time + '\n\n'; + count++; + } + } + } + return { content: [{ type: 'text', text: output }] }; + } +); + +server.tool( + 'get-train-route-stations', + '查询特定列车车次在指定区间内的途径车站、到站时间、出发时间及停留时间等详细经停信息。当用户询问某趟具体列车的经停站时使用此接口。', + { + trainNo: z.string().describe('要查询的实际车次编号 `train_no`,例如 "240000G10336",而非"G1033"。此编号通常可以从 `get-tickets` 的查询结果中获取,或者由用户直接提供。'), + fromStationTelecode: z.string().describe('该列车行程的**出发站**的 `station_telecode` (3位字母编码`)。通常来自 `get-tickets` 结果中的 `telecode` 字段,或者通过 `get-station-code-by-names` 得到。'), + toStationTelecode: z.string().describe('该列车行程的**到达站**的 `station_telecode` (3位字母编码)。通常来自 `get-tickets` 结果中的 `telecode` 字段,或者通过 `get-station-code-by-names` 得到。'), + departDate: z.string().length(10).describe('列车从 `fromStationTelecode` 指定的车站出发的日期 (格式: yyyy-MM-dd)。如果用户提供的是相对日期,请务必先调用 `get-current-date` 解析。'), + }, + async ({ trainNo }) => { + const result = await pool.query( + `SELECT station_no, station_telecode, station_name, arrive_time, depart_time, stopover_time + FROM train.train_routes WHERE train_no = $1 ORDER BY station_no`, + [trainNo] + ); + if (result.rows.length === 0) return { content: [{ type: 'text', text: '未查询到相关车次信息。' }] }; + const routeInfo: RouteStationInfo[] = result.rows.map(r => ({ + arrive_time: r.arrive_time, + station_name: r.station_name, + stopover_time: r.stopover_time, + station_no: r.station_no, + })); + return { content: [{ type: 'text', text: JSON.stringify(routeInfo) }] }; + } +); + +// --------------------------------------------------------------------------- +// Startup +// --------------------------------------------------------------------------- +async function startServer() { + await loadStations(); + console.error(`12306 MCP Server (pg-backed) started. Loaded ${Object.keys(STATIONS).length} stations.`); + + program + .name('mcp-server-12306') + .version(VERSION) + .option('--stdio', 'use stdio transport (default)', true) + .parse(process.argv); + + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +startServer().catch(err => { + console.error('Failed to start server:', err); + process.exit(1); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/types.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/types.ts new file mode 100644 index 00000000..c0371f0d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/types.ts @@ -0,0 +1,302 @@ +#!/usr/bin/env node +export type TicketData = { + secret_Sstr: string; + button_text_info: string; + train_no: string; + station_train_code: string; + start_station_telecode: string; + end_station_telecode: string; + from_station_telecode: string; + to_station_telecode: string; + start_time: string; + arrive_time: string; + lishi: string; + canWebBuy: string; + yp_info: string; + start_train_date: string; + train_seat_feature: string; + location_code: string; + from_station_no: string; + to_station_no: string; + is_support_card: string; + controlled_train_flag: string; + gg_num: string; + gr_num: string; + qt_num: string; + rw_num: string; + rz_num: string; + tz_num: string; + wz_num: string; + yb_num: string; + yw_num: string; + yz_num: string; + ze_num: string; + zy_num: string; + swz_num: string; + srrb_num: string; + yp_ex: string; + seat_types: string; + exchange_train_flag: string; + houbu_train_flag: string; + houbu_seat_limit: string; + yp_info_new: string; + '40': string; + '41': string; + '42': string; + '43': string; + '44': string; + '45': string; + dw_flag: string; + '47': string; + stopcheckTime: string; + country_flag: string; + local_arrive_time: string; + local_start_time: string; + '52': string; + bed_level_info: string; + seat_discount_info: string; + sale_time: string; + '56': string; +}; + +export const TicketDataKeys: (keyof TicketData)[] = [ + 'secret_Sstr', + 'button_text_info', + 'train_no', + 'station_train_code', + 'start_station_telecode', + 'end_station_telecode', + 'from_station_telecode', + 'to_station_telecode', + 'start_time', + 'arrive_time', + 'lishi', + 'canWebBuy', + 'yp_info', + 'start_train_date', + 'train_seat_feature', + 'location_code', + 'from_station_no', + 'to_station_no', + 'is_support_card', + 'controlled_train_flag', + 'gg_num', + 'gr_num', + 'qt_num', + 'rw_num', + 'rz_num', + 'tz_num', + 'wz_num', + 'yb_num', + 'yw_num', + 'yz_num', + 'ze_num', + 'zy_num', + 'swz_num', + 'srrb_num', + 'yp_ex', + 'seat_types', + 'exchange_train_flag', + 'houbu_train_flag', + 'houbu_seat_limit', + 'yp_info_new', + '40', + '41', + '42', + '43', + '44', + '45', + 'dw_flag', + '47', + 'stopcheckTime', + 'country_flag', + 'local_arrive_time', + 'local_start_time', + '52', + 'bed_level_info', + 'seat_discount_info', + 'sale_time', + '56', +]; + +export type TicketInfo = { + train_no: string; + start_train_code: string; + start_date: string; + start_time: string; + arrive_date: string; + arrive_time: string; + lishi: string; + from_station: string; + to_station: string; + from_station_telecode: string; + to_station_telecode: string; + prices: Price[]; + dw_flag: string[]; +}; + +export type StationData = { + station_id: string; + station_name: string; + station_code: string; + station_pinyin: string; + station_short: string; + station_index: string; + code: string; + city: string; + r1: string; + r2: string; +}; + +export const StationDataKeys: (keyof StationData)[] = [ + 'station_id', + 'station_name', + 'station_code', + 'station_pinyin', + 'station_short', + 'station_index', + 'code', + 'city', + 'r1', + 'r2', +]; + +export interface Price { + seat_name: string; + short: string; + seat_type_code: string; + num: string; + price: number; + discount: number | null; +} + +export type RouteStationData = { + arrive_time: string; + station_name: string; + isChina: string; + start_time: string; + stopover_time: string; + station_no: string; + country_code: string; + country_name: string; + isEnabled: boolean; + train_class_name?: string; + service_type?: string; + end_station_name?: string; + start_station_name?: string; + station_train_code?: string; +}; + +export type RouteStationInfo = { + arrive_time: string; + station_name: string; + stopover_time: string; + station_no: number; +}; + +export type InterlineData = { + all_lishi: string; + all_lishi_minutes: number; + arrive_date: string; + arrive_time: string; + end_station_code: string; + end_station_name: string; + first_train_no: string; + from_station_code: string; + from_station_name: string; + fullList: InterlineTicketData[]; + isHeatTrain: string; + isOutStation: string; + lCWaitTime: string; + lishi_flag: string; + middle_date: string; + middle_station_code: string; + middle_station_name: string; + same_station: string; + same_train: string; + score: number; + score_str: string; + scretstr: string; + second_train_no: string; + start_time: string; + train_count: number; + train_date: string; // 出发时间 + use_time: string; + wait_time: string; + wait_time_minutes: number; +}; + +export type InterlineInfo = { + lishi: string; + //all_lishi_minutes: number; + start_time: string; + start_date: string; + middle_date: string; + arrive_date: string; + arrive_time: string; + from_station_code: string; + from_station_name: string; + middle_station_code: string; + middle_station_name: string; + end_station_code: string; + end_station_name: string; + start_train_code: string; // 用于过滤 + first_train_no: string; + second_train_no: string; + train_count: number; + ticketList: TicketInfo[]; + //isHeatTrain: string; + //isOutStation: string; + //lCWaitTime: string; + //lishi_flag: string; + same_station: boolean; + same_train: boolean; + wait_time: string; + //wait_time_minutes: number; +}; + +export type InterlineTicketData = { + arrive_time: string; + bed_level_info: string; + controlled_train_flag: string; + country_flag: string; + day_difference: string; + dw_flag: string; + end_station_name: string; + end_station_telecode: string; + from_station_name: string; + from_station_no: string; + from_station_telecode: string; + gg_num: string; + gr_num: string; + is_support_card: string; + lishi: string; + local_arrive_time: string; + local_start_time: string; + qt_num: string; + rw_num: string; + rz_num: string; + seat_discount_info: string; + seat_types: string; + srrb_num: string; + start_station_name: string; + start_station_telecode: string; + start_time: string; + start_train_date: string; + station_train_code: string; + swz_num: string; + to_station_name: string; + to_station_no: string; + to_station_telecode: string; + train_no: string; + train_seat_feature: string; + trms_train_flag: string; + tz_num: string; + wz_num: string; + yb_num: string; + yp_info: string; + yw_num: string; + yz_num: string; + ze_num: string; + zy_num: string; +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/tsconfig.json new file mode 100644 index 00000000..1c7e3c36 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/.gitignore new file mode 100644 index 00000000..6d370fc5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/.gitignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build +build/ +dist/ +*.tsbuildinfo + +# Environment +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Project specific +gcp-oauth.keys.json +.calendar-mcp/ +credentials.json +.calendar-server-credentials.json \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/Dockerfile new file mode 100644 index 00000000..7a61c37a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Create data directory +RUN mkdir -p /app/calendar-data + +# Set permissions for the data directory +RUN chown -R node:node /app/calendar-data + +# Switch to non-root user +USER node + +# Start the server +CMD ["node", "build/index.js"] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/LICENSE new file mode 100644 index 00000000..31c022e6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 GongRzhe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/README.md new file mode 100644 index 00000000..746c8a8d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/README.md @@ -0,0 +1,217 @@ +# Calendar AutoAuth MCP Server + +A Model Context Protocol (MCP) server for Google Calendar integration in Cluade Desktop with auto authentication support. This server enables AI assistants to manage Google Calendar events through natural language interactions. + +![](https://badge.mcpx.dev?type=server 'MCP Server') +[![smithery badge](https://smithery.ai/badge/@gongrzhe/server-calendar-autoauth-mcp)](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp) +[![npm version](https://badge.fury.io/js/%40gongrzhe%2Fserver-calendar-autoauth-mcp.svg)](https://www.npmjs.com/package/@gongrzhe/server-calendar-autoauth-mcp) +[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC) + +## Features + +- Create calendar events with title, time, description, and location +- Retrieve event details by event ID +- Update existing events (title, time, description, location) +- Delete events +- List events within a specified time range +- Full integration with Google Calendar API +- Simple OAuth2 authentication flow with auto browser launch +- Support for both Desktop and Web application credentials +- Global credential storage for convenience + +## Installation & Authentication + +### Installing via Smithery + +To install Calendar AutoAuth Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp): + +```bash +npx -y @smithery/cli install @gongrzhe/server-calendar-autoauth-mcp --client claude +``` + +1. Create a Google Cloud Project and obtain credentials: + + a. Create a Google Cloud Project: + - Go to [Google Cloud Console](https://console.cloud.google.com/) + - Create a new project or select an existing one + - Enable the Google Calendar API for your project + + b. Create OAuth 2.0 Credentials: + - Go to "APIs & Services" > "Credentials" + - Click "Create Credentials" > "OAuth client ID" + - Choose either "Desktop app" or "Web application" as application type + - Give it a name and click "Create" + - For Web application, add `http://localhost:3000/oauth2callback` to the authorized redirect URIs + - Download the JSON file of your client's OAuth keys + - Rename the key file to `gcp-oauth.keys.json` + +2. Run Authentication: + + You can authenticate in two ways: + + a. Global Authentication (Recommended): + ```bash + # First time: Place gcp-oauth.keys.json in your home directory's .calendar-mcp folder + mkdir -p ~/.calendar-mcp + mv gcp-oauth.keys.json ~/.calendar-mcp/ + + # Run authentication from anywhere + npx @gongrzhe/server-calendar-autoauth-mcp auth + ``` + + b. Local Authentication: + ```bash + # Place gcp-oauth.keys.json in your current directory + # The file will be automatically copied to global config + npx @gongrzhe/server-calendar-autoauth-mcp auth + ``` + + The authentication process will: + - Look for `gcp-oauth.keys.json` in the current directory or `~/.calendar-mcp/` + - If found in current directory, copy it to `~/.calendar-mcp/` + - Open your default browser for Google authentication + - Save credentials as `~/.calendar-mcp/credentials.json` + + > **Note**: + > - After successful authentication, credentials are stored globally in `~/.calendar-mcp/` and can be used from any directory + > - Both Desktop app and Web application credentials are supported + > - For Web application credentials, make sure to add `http://localhost:3000/oauth2callback` to your authorized redirect URIs + +3. Configure in Claude Desktop: + +```json +{ + "mcpServers": { + "calendar": { + "command": "npx", + "args": [ + "@gongrzhe/server-calendar-autoauth-mcp" + ] + } + } +} +``` + +### Docker Support + +If you prefer using Docker: + +1. Authentication: +```bash +docker run -i --rm \ + --mount type=bind,source=/path/to/gcp-oauth.keys.json,target=/gcp-oauth.keys.json \ + -v mcp-calendar:/calendar-server \ + -e CALENDAR_OAUTH_PATH=/gcp-oauth.keys.json \ + -e "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json" \ + -p 3000:3000 \ + mcp/calendar auth +``` + +2. Usage: +```json +{ + "mcpServers": { + "calendar": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-v", + "mcp-calendar:/calendar-server", + "-e", + "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json", + "mcp/calendar" + ] + } + } +} +``` + +## Usage Examples + +The server provides several tools that can be used through the Claude Desktop: + +### Create Event +```json +{ + "summary": "Team Meeting", + "start": { + "dateTime": "2024-01-20T10:00:00Z" + }, + "end": { + "dateTime": "2024-01-20T11:00:00Z" + }, + "description": "Weekly team sync", + "location": "Conference Room A" +} +``` + +### List Events +```json +{ + "timeMin": "2024-01-01T00:00:00Z", + "timeMax": "2024-12-31T23:59:59Z", + "maxResults": 10, + "orderBy": "startTime" +} +``` + +### Update Event +```json +{ + "eventId": "event123", + "summary": "Updated Meeting Title", + "start": { + "dateTime": "2024-01-20T11:00:00Z" + }, + "end": { + "dateTime": "2024-01-20T12:00:00Z" + } +} +``` + +### Delete Event +```json +{ + "eventId": "event123" +} +``` + +## Security Notes + +- OAuth credentials are stored securely in your local environment (`~/.calendar-mcp/`) +- The server uses offline access to maintain persistent authentication +- Never share or commit your credentials to version control +- Regularly review and revoke unused access in your Google Account settings +- Credentials are stored globally but are only accessible by the current user + +## Troubleshooting + +1. **OAuth Keys Not Found** + - Make sure `gcp-oauth.keys.json` is in either your current directory or `~/.calendar-mcp/` + - Check file permissions + +2. **Invalid Credentials Format** + - Ensure your OAuth keys file contains either `web` or `installed` credentials + - For web applications, verify the redirect URI is correctly configured + +3. **Port Already in Use** + - If port 3000 is already in use, please free it up before running authentication + - You can find and stop the process using that port + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the ISC License. + +## Author + +gongrzhe + +## Support + +If you encounter any issues or have questions, please file an issue on the GitHub repository. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/docker-compose.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/docker-compose.yml new file mode 100644 index 00000000..7867583e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + redis: + image: redis:latest + container_name: my-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + restart: always + +volumes: + redis_data: \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package-lock.json new file mode 100644 index 00000000..8d3776f4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package-lock.json @@ -0,0 +1,985 @@ +{ + "name": "@gongrzhe/server-calendar-autoauth-mcp", + "version": "1.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gongrzhe/server-calendar-autoauth-mcp", + "version": "1.0.2", + "license": "ISC", + "dependencies": { + "@modelcontextprotocol/sdk": "^0.4.0", + "googleapis": "^133.0.0", + "open": "^8.4.2", + "pg": "^8.13.0", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.22.4" + }, + "bin": { + "server-calendar-autoauth-mcp": "build/index.js" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/open": "^6.2.1", + "@types/pg": "^8.18.0", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.4.0", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/open": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@types/open/-/open-6.2.1.tgz", + "integrity": "sha512-CzV16LToFaKwm1FfplVTF08E3pznw4fQNCQ87N+A1RU00zu/se7npvb6IC9db3/emnSThQ6R8qFKgrei2M4EYQ==", + "deprecated": "This is a stub types definition. open provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.0.tgz", + "integrity": "sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "133.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-133.0.0.tgz", + "integrity": "sha512-6xyc49j+x7N4smawJs/q1i7mbSkt6SYUWWd9RbsmmDW7gRv+mhwZ4xT+XkPihZcNyo/diF//543WZq4szdS74w==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/qs": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", + "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package.json new file mode 100644 index 00000000..ced72e4a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package.json @@ -0,0 +1,60 @@ +{ + "name": "@gongrzhe/server-calendar-autoauth-mcp", + "version": "1.0.2", + "description": "A Model Context Protocol server for Google Calendar integration with auto authentication", + "main": "build/index.js", + "type": "module", + "bin": { + "server-calendar-autoauth-mcp": "./build/index.js" + }, + "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build", + "auth": "node ./build/index.js auth" + }, + "files": [ + "build", + "README.md" + ], + "keywords": [ + "calendar", + "events", + "scheduling", + "mcp", + "model-context-protocol", + "google-calendar", + "claude", + "cursor", + "auto-auth" + ], + "author": "gongrzhe", + "license": "ISC", + "repository": { + "type": "git", + "url": "git+https://github.com/gongrzhe/server-calendar-autoauth-mcp.git" + }, + "bugs": { + "url": "https://github.com/gongrzhe/server-calendar-autoauth-mcp/issues" + }, + "homepage": "https://github.com/gongrzhe/server-calendar-autoauth-mcp#readme", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/open": "^6.2.1", + "@types/pg": "^8.18.0", + "typescript": "^5.7.2" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^0.4.0", + "googleapis": "^133.0.0", + "open": "^8.4.2", + "pg": "^8.13.0", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/smithery.yaml new file mode 100644 index 00000000..d0023504 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/smithery.yaml @@ -0,0 +1,23 @@ +# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml + +startCommand: + type: stdio + configSchema: + # JSON Schema defining the configuration options for the MCP. + type: object + required: + - calendarOauthPath + - calendarCredentialsPath + properties: + calendarOauthPath: + type: string + default: ~/.calendar-mcp/gcp-oauth.keys.json + description: Path to the Google OAuth credentials file. + calendarCredentialsPath: + type: string + default: ~/.calendar-mcp/credentials.json + description: Path where the OAuth tokens will be stored. + commandFunction: + # A function that produces the CLI command to start the MCP on stdio. + |- + (config) => ({ command: 'node', args: ['build/index.js'], env: { CALENDAR_OAUTH_PATH: config.calendarOauthPath, CALENDAR_CREDENTIALS_PATH: config.calendarCredentialsPath } }) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/index.ts new file mode 100644 index 00000000..52d4bed2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/index.ts @@ -0,0 +1,422 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import pg from 'pg'; + +const { Pool } = pg; + +// PostgreSQL connection pool +const pool = new Pool({ + host: process.env.PG_HOST || 'localhost', + port: parseInt(process.env.PG_PORT || '5432'), + database: process.env.PG_DATABASE || 'toolathlon', + user: process.env.PG_USER || 'postgres', + password: process.env.PG_PASSWORD || 'postgres', +}); + +// Helper to format a DB row into Google Calendar event JSON +function formatEvent(row: any) { + return { + id: row.id, + summary: row.summary, + description: row.description, + location: row.location, + start: { + dateTime: row.start_datetime ? new Date(row.start_datetime).toISOString() : null, + timeZone: row.start_timezone || undefined, + }, + end: { + dateTime: row.end_datetime ? new Date(row.end_datetime).toISOString() : null, + timeZone: row.end_timezone || undefined, + }, + status: row.status, + htmlLink: row.html_link, + creator: row.creator, + organizer: row.organizer, + attendees: row.attendees, + recurrence: row.recurrence, + reminders: row.reminders, + created: row.created ? new Date(row.created).toISOString() : null, + updated: row.updated ? new Date(row.updated).toISOString() : null, + }; +} + +// PgCalendar class that mimics the google calendar.events.* interface +class PgCalendar { + events: { + insert: (params: { calendarId: string; requestBody: any }) => Promise<{ data: any }>; + get: (params: { calendarId: string; eventId: string }) => Promise<{ data: any }>; + patch: (params: { calendarId: string; eventId: string; requestBody: any }) => Promise<{ data: any }>; + delete: (params: { calendarId: string; eventId: string }) => Promise<{ data: any }>; + list: (params: { calendarId: string; timeMin?: string; timeMax?: string; maxResults?: number; orderBy?: string; singleEvents?: boolean }) => Promise<{ data: { items: any[] } }>; + }; + + constructor(pool: pg.Pool) { + const self = this; + + this.events = { + async insert({ calendarId, requestBody }: { calendarId: string; requestBody: any }) { + const startDateTime = requestBody.start?.dateTime; + const startTimeZone = requestBody.start?.timeZone || null; + const endDateTime = requestBody.end?.dateTime; + const endTimeZone = requestBody.end?.timeZone || null; + const result = await pool.query( + `INSERT INTO gcal.events (summary, description, location, start_datetime, start_timezone, end_datetime, end_timezone) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *`, + [ + requestBody.summary || null, + requestBody.description || null, + requestBody.location || null, + startDateTime, + startTimeZone, + endDateTime, + endTimeZone, + ] + ); + return { data: formatEvent(result.rows[0]) }; + }, + + async get({ calendarId, eventId }: { calendarId: string; eventId: string }) { + const result = await pool.query( + `SELECT * FROM gcal.events WHERE id = $1`, + [eventId] + ); + if (result.rows.length === 0) { + throw new Error(`Event not found: ${eventId}`); + } + return { data: formatEvent(result.rows[0]) }; + }, + + async patch({ calendarId, eventId, requestBody }: { calendarId: string; eventId: string; requestBody: any }) { + const setClauses: string[] = []; + const values: any[] = []; + let paramIndex = 1; + + if (requestBody.summary !== undefined) { + setClauses.push(`summary = $${paramIndex++}`); + values.push(requestBody.summary); + } + if (requestBody.description !== undefined) { + setClauses.push(`description = $${paramIndex++}`); + values.push(requestBody.description); + } + if (requestBody.location !== undefined) { + setClauses.push(`location = $${paramIndex++}`); + values.push(requestBody.location); + } + if (requestBody.start?.dateTime !== undefined) { + setClauses.push(`start_datetime = $${paramIndex++}`); + values.push(requestBody.start.dateTime); + } + if (requestBody.start?.timeZone !== undefined) { + setClauses.push(`start_timezone = $${paramIndex++}`); + values.push(requestBody.start.timeZone); + } + if (requestBody.end?.dateTime !== undefined) { + setClauses.push(`end_datetime = $${paramIndex++}`); + values.push(requestBody.end.dateTime); + } + if (requestBody.end?.timeZone !== undefined) { + setClauses.push(`end_timezone = $${paramIndex++}`); + values.push(requestBody.end.timeZone); + } + + // Always update the updated timestamp + setClauses.push(`updated = NOW()`); + + if (setClauses.length === 1) { + // Only the updated timestamp, no real changes; just fetch + const result = await pool.query(`SELECT * FROM gcal.events WHERE id = $1`, [eventId]); + if (result.rows.length === 0) throw new Error(`Event not found: ${eventId}`); + return { data: formatEvent(result.rows[0]) }; + } + + values.push(eventId); + const result = await pool.query( + `UPDATE gcal.events SET ${setClauses.join(', ')} WHERE id = $${paramIndex} RETURNING *`, + values + ); + if (result.rows.length === 0) { + throw new Error(`Event not found: ${eventId}`); + } + return { data: formatEvent(result.rows[0]) }; + }, + + async delete({ calendarId, eventId }: { calendarId: string; eventId: string }) { + const result = await pool.query( + `DELETE FROM gcal.events WHERE id = $1`, + [eventId] + ); + if (result.rowCount === 0) { + throw new Error(`Event not found: ${eventId}`); + } + return { data: {} }; + }, + + async list({ calendarId, timeMin, timeMax, maxResults, orderBy, singleEvents }: { + calendarId: string; + timeMin?: string; + timeMax?: string; + maxResults?: number; + orderBy?: string; + singleEvents?: boolean; + }) { + const conditions: string[] = []; + const values: any[] = []; + let paramIndex = 1; + + if (timeMin) { + conditions.push(`start_datetime >= $${paramIndex++}`); + values.push(timeMin); + } + if (timeMax) { + conditions.push(`end_datetime <= $${paramIndex++}`); + values.push(timeMax); + } + + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + + let orderClause = 'ORDER BY start_datetime ASC'; + if (orderBy === 'updated') { + orderClause = 'ORDER BY updated DESC'; + } + + const limitClause = maxResults ? `LIMIT $${paramIndex++}` : ''; + if (maxResults) { + values.push(maxResults); + } + + const result = await pool.query( + `SELECT * FROM gcal.events ${whereClause} ${orderClause} ${limitClause}`, + values + ); + + return { data: { items: result.rows.map(formatEvent) } }; + }, + }; + } +} + +// Schema definitions +const CreateEventSchema = z.object({ + summary: z.string().describe("Event title"), + start: z.object({ + dateTime: z.string().describe("Start time (ISO format)"), + timeZone: z.string().optional().describe("Time zone"), + }), + end: z.object({ + dateTime: z.string().describe("End time (ISO format)"), + timeZone: z.string().optional().describe("Time zone"), + }), + description: z.string().optional().describe("Event description"), + location: z.string().optional().describe("Event location"), +}); + +const GetEventSchema = z.object({ + eventId: z.string().describe("ID of the event to retrieve"), +}); + +const UpdateEventSchema = z.object({ + eventId: z.string().describe("ID of the event to update"), + summary: z.string().optional().describe("New event title"), + start: z.object({ + dateTime: z.string().describe("New start time (ISO format)"), + timeZone: z.string().optional().describe("Time zone"), + }).optional(), + end: z.object({ + dateTime: z.string().describe("New end time (ISO format)"), + timeZone: z.string().optional().describe("Time zone"), + }).optional(), + description: z.string().optional().describe("New event description"), + location: z.string().optional().describe("New event location"), +}); + +const DeleteEventSchema = z.object({ + eventId: z.string().describe("ID of the event to delete"), +}); + +const ListEventsSchema = z.object({ + timeMin: z.string().describe("Start of time range (ISO format)"), + timeMax: z.string().describe("End of time range (ISO format)"), + maxResults: z.number().optional().describe("Maximum number of events to return"), + orderBy: z.enum(['startTime', 'updated']).optional().describe("Sort order"), +}); + +// Main function +async function main() { + // Initialize PgCalendar + const calendar = new PgCalendar(pool); + const calendarId = 'primary'; + + // Server implementation + const server = new Server({ + name: "google-calendar", + version: "1.0.0", + capabilities: { + tools: {}, + }, + }); + + // Tool handlers + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_event", + description: "Creates a new event in Google Calendar", + inputSchema: zodToJsonSchema(CreateEventSchema), + }, + { + name: "get_event", + description: "Retrieves details of a specific event", + inputSchema: zodToJsonSchema(GetEventSchema), + }, + { + name: "update_event", + description: "Updates an existing event", + inputSchema: zodToJsonSchema(UpdateEventSchema), + }, + { + name: "delete_event", + description: "Deletes an event from the calendar", + inputSchema: zodToJsonSchema(DeleteEventSchema), + }, + { + name: "list_events", + description: "Lists events within a specified time range", + inputSchema: zodToJsonSchema(ListEventsSchema), + }, + ], + })); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + switch (name) { + case "create_event": { + const validatedArgs = CreateEventSchema.parse(args); + const response = await calendar.events.insert({ + calendarId, + requestBody: validatedArgs, + }); + return { + content: [ + { + type: "text", + text: `Event created with ID: ${response.data.id}\n` + + `Title: ${validatedArgs.summary}\n` + + `Start: ${validatedArgs.start.dateTime}\n` + + `End: ${validatedArgs.end.dateTime}`, + }, + ], + }; + } + + case "get_event": { + const validatedArgs = GetEventSchema.parse(args); + const response = await calendar.events.get({ + calendarId, + eventId: validatedArgs.eventId, + }); + return { + content: [ + { + type: "text", + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + } + + case "update_event": { + const validatedArgs = UpdateEventSchema.parse(args); + const { eventId, ...updates } = validatedArgs; + const response = await calendar.events.patch({ + calendarId, + eventId, + requestBody: updates, + }); + return { + content: [ + { + type: "text", + text: `Event updated: ${eventId}\n` + + `New title: ${updates.summary || '(unchanged)'}\n` + + `New start: ${updates.start?.dateTime || '(unchanged)'}\n` + + `New end: ${updates.end?.dateTime || '(unchanged)'}`, + }, + ], + }; + } + + case "delete_event": { + const validatedArgs = DeleteEventSchema.parse(args); + await calendar.events.delete({ + calendarId, + eventId: validatedArgs.eventId, + }); + return { + content: [ + { + type: "text", + text: `Event deleted: ${validatedArgs.eventId}`, + }, + ], + }; + } + + case "list_events": { + const validatedArgs = ListEventsSchema.parse(args); + const response = await calendar.events.list({ + calendarId, + timeMin: validatedArgs.timeMin, + timeMax: validatedArgs.timeMax, + maxResults: validatedArgs.maxResults || 10, + orderBy: validatedArgs.orderBy || 'startTime', + singleEvents: true, + }); + return { + content: [ + { + type: "text", + text: `Found ${response.data.items?.length || 0} events:\n` + + JSON.stringify(response.data.items, null, 2), + }, + ], + }; + } + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + }); + + // Start the server + const transport = new StdioServerTransport(); + server.connect(transport).catch((error) => { + console.error("Fatal error running server:", error); + process.exit(1); + }); + console.error('Google Calendar MCP Server running on stdio'); +} + +main().catch(console.error); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/pg-calendar.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/pg-calendar.ts new file mode 100644 index 00000000..d9eb98e5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/pg-calendar.ts @@ -0,0 +1,239 @@ +import pg from 'pg'; +const { Pool } = pg; + +export interface CalendarEvent { + id: string; + summary: string | null; + description: string | null; + location: string | null; + start: { dateTime: string | null; timeZone?: string }; + end: { dateTime: string | null; timeZone?: string }; + status: string | null; + htmlLink: string | null; + creator: any; + organizer: any; + attendees: any; + recurrence: any; + reminders: any; + created: string | null; + updated: string | null; +} + +interface EventRow { + id: string; + summary: string | null; + description: string | null; + location: string | null; + start_datetime: string | null; + start_timezone: string | null; + end_datetime: string | null; + end_timezone: string | null; + status: string | null; + html_link: string | null; + creator: any; + organizer: any; + attendees: any; + recurrence: any; + reminders: any; + created: string | null; + updated: string | null; +} + +function formatEvent(row: EventRow): CalendarEvent { + return { + id: row.id, + summary: row.summary, + description: row.description, + location: row.location, + start: { + dateTime: row.start_datetime ? new Date(row.start_datetime).toISOString() : null, + timeZone: row.start_timezone || undefined, + }, + end: { + dateTime: row.end_datetime ? new Date(row.end_datetime).toISOString() : null, + timeZone: row.end_timezone || undefined, + }, + status: row.status, + htmlLink: row.html_link, + creator: row.creator, + organizer: row.organizer, + attendees: row.attendees, + recurrence: row.recurrence, + reminders: row.reminders, + created: row.created ? new Date(row.created).toISOString() : null, + updated: row.updated ? new Date(row.updated).toISOString() : null, + }; +} + +export class PgCalendar { + events: { + insert(params: { calendarId: string; requestBody: any }): Promise<{ data: CalendarEvent }>; + get(params: { calendarId: string; eventId: string }): Promise<{ data: CalendarEvent }>; + patch(params: { calendarId: string; eventId: string; requestBody: any }): Promise<{ data: CalendarEvent }>; + delete(params: { calendarId: string; eventId: string }): Promise<{ data: {} }>; + list(params: { + calendarId: string; + timeMin?: string; + timeMax?: string; + maxResults?: number; + orderBy?: string; + singleEvents?: boolean; + }): Promise<{ data: { items: CalendarEvent[] } }>; + }; + + constructor(pool: InstanceType) { + this.events = { + async insert({ calendarId, requestBody }) { + const startDateTime = requestBody.start?.dateTime; + const startTimeZone = requestBody.start?.timeZone || null; + const endDateTime = requestBody.end?.dateTime; + const endTimeZone = requestBody.end?.timeZone || null; + const attendeesJson = requestBody.attendees + ? JSON.stringify(requestBody.attendees) + : '[]'; + + const result = await pool.query( + `INSERT INTO gcal.events (summary, description, location, start_datetime, start_timezone, end_datetime, end_timezone, attendees) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb) + RETURNING *`, + [ + requestBody.summary || null, + requestBody.description || null, + requestBody.location || null, + startDateTime, + startTimeZone, + endDateTime, + endTimeZone, + attendeesJson, + ] + ); + return { data: formatEvent(result.rows[0]) }; + }, + + async get({ calendarId, eventId }) { + const result = await pool.query( + `SELECT * FROM gcal.events WHERE id = $1`, + [eventId] + ); + if (result.rows.length === 0) { + throw new Error(`Event not found: ${eventId}`); + } + return { data: formatEvent(result.rows[0]) }; + }, + + async patch({ calendarId, eventId, requestBody }) { + const setClauses: string[] = []; + const values: any[] = []; + let paramIndex = 1; + + if (requestBody.summary !== undefined) { + setClauses.push(`summary = $${paramIndex++}`); + values.push(requestBody.summary); + } + if (requestBody.description !== undefined) { + setClauses.push(`description = $${paramIndex++}`); + values.push(requestBody.description); + } + if (requestBody.location !== undefined) { + setClauses.push(`location = $${paramIndex++}`); + values.push(requestBody.location); + } + if (requestBody.start?.dateTime !== undefined) { + setClauses.push(`start_datetime = $${paramIndex++}`); + values.push(requestBody.start.dateTime); + } + if (requestBody.start?.timeZone !== undefined) { + setClauses.push(`start_timezone = $${paramIndex++}`); + values.push(requestBody.start.timeZone); + } + if (requestBody.end?.dateTime !== undefined) { + setClauses.push(`end_datetime = $${paramIndex++}`); + values.push(requestBody.end.dateTime); + } + if (requestBody.end?.timeZone !== undefined) { + setClauses.push(`end_timezone = $${paramIndex++}`); + values.push(requestBody.end.timeZone); + } + + // Always update the updated timestamp + setClauses.push(`updated = NOW()`); + + if (setClauses.length === 1) { + // Only the updated timestamp, no real changes; just fetch + const result = await pool.query( + `SELECT * FROM gcal.events WHERE id = $1`, + [eventId] + ); + if (result.rows.length === 0) throw new Error(`Event not found: ${eventId}`); + return { data: formatEvent(result.rows[0]) }; + } + + values.push(eventId); + const result = await pool.query( + `UPDATE gcal.events SET ${setClauses.join(', ')} WHERE id = $${paramIndex} RETURNING *`, + values + ); + if (result.rows.length === 0) { + throw new Error(`Event not found: ${eventId}`); + } + return { data: formatEvent(result.rows[0]) }; + }, + + async delete({ calendarId, eventId }) { + const result = await pool.query( + `DELETE FROM gcal.events WHERE id = $1`, + [eventId] + ); + if (result.rowCount === 0) { + throw new Error(`Event not found: ${eventId}`); + } + return { data: {} }; + }, + + async list({ calendarId, timeMin, timeMax, maxResults, orderBy, singleEvents }) { + const conditions: string[] = []; + const values: any[] = []; + let paramIndex = 1; + + if (timeMin) { + conditions.push(`start_datetime >= $${paramIndex++}`); + values.push(timeMin); + } + if (timeMax) { + conditions.push(`end_datetime <= $${paramIndex++}`); + values.push(timeMax); + } + + const whereClause = conditions.length > 0 + ? `WHERE ${conditions.join(' AND ')}` + : ''; + + let orderClause = 'ORDER BY start_datetime ASC'; + if (orderBy === 'updated') { + orderClause = 'ORDER BY updated DESC'; + } + + const limitClause = maxResults ? `LIMIT $${paramIndex++}` : ''; + if (maxResults) { + values.push(maxResults); + } + + const result = await pool.query( + `SELECT * FROM gcal.events ${whereClause} ${orderClause} ${limitClause}`, + values + ); + return { data: { items: result.rows.map(formatEvent) } }; + }, + }; + } +} + +export function createPool(): InstanceType { + return new Pool({ + host: process.env.PG_HOST || 'localhost', + port: parseInt(process.env.PG_PORT || '5432'), + database: process.env.PG_DATABASE || 'toolathlon', + user: process.env.PG_USER || 'postgres', + password: process.env.PG_PASSWORD || 'postgres', + }); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/tsconfig.json new file mode 100644 index 00000000..3e3350f9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "node", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build"] + } + \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/.gitignore new file mode 100644 index 00000000..b736b4a1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +package-lock.json diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/HowToCook-mcp.dxt b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/HowToCook-mcp.dxt new file mode 100644 index 00000000..720879d3 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/HowToCook-mcp.dxt differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README.md new file mode 100644 index 00000000..be2af351 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README.md @@ -0,0 +1,227 @@ +# 🍳 HowToCook-MCP Server 🥘 -- 炫一周好饭,拒绝拼好饭 + +[English](./README_EN.md) | 简体中文 + +
+ +本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助 + +[亚洲最佳 CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github) + + + +
+ +> 让 AI 助手变身私人大厨,为你的一日三餐出谋划策! + +基于[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook)打造的 MCP(Model Context Protocol)服务器,让 AI 助手能够为你推荐菜谱、规划膳食,解决"今天吃什么"的世纪难题! + +数据来源:[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ 没有 star 的同学快去点个星星吧! + +🎉 **想直接使用当前 MCP?立即体验** [https://howtocookmcp.weilei.site/](https://howtocookmcp.weilei.site/) + +🎉 **同时,我们也提供了 DXT(Desktop Extensions)供大家体验,一键安装到 Claude Desktop** + +如下:请确保你已经安装了最新版的 Claude Desktop, 当前 MCP 的 DXT 文件已上传代码库,可以自行下载或者 Fork 本仓库自行构建 + +![DXT](./public/dxt.png) +![DXT](./public/dxt2.png) +![DXT](./public/dxt3.png) + +本地开发如何打包成 DXT? + +1.运行 `npm install -g @anthropic-ai/dxt` + +2.在包含本地 MCP 服务器的文件夹中,运行 `dxt init`。也就是您 MCP 的根目录,此命令将引导您创建`manifest.json` + +3.运行`dxt pack`创建 dxt 文件 + +现在,任何支持 DXT 的应用都可以运行您的本地 MCP 服务器。例如,使用适用于 macOS 和 Windows 的 Claude 打开该文件即可显示安装对话框 + +具体参阅:[anthropics/dxt](https://github.com/anthropics/dxt) + +## 📸 效果预览 + +![功能预览1](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/1.png) +![功能预览2](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/2.png) + +## 🔌 支持的 MCP 客户端 + +本服务器适用于所有支持 MCP 协议的 AI 助手和客户端,包括但不限于: + +- 🤖 Claude 桌面应用 +- 📝 Cursor +- 💼 其他支持 MCP 的客户端 + +## ✨ 美味功能 + +该 MCP 服务器提供以下美食工具: + +1. **📚 查询全部菜谱** - 获取所有可用菜谱数据,做菜百科全书 -- 慎用这个--上下文太大 +2. **🔍 根据分类查询菜谱** - 按照分类筛选菜谱,想吃水产?早餐?荤菜?主食?一键搞定! +3. **📖 查询指定菜谱** - 根据菜谱名称查询特定菜谱的完整详情,包括食材、步骤等 +4. **🧩 智能推荐膳食** - 根据你的忌口、过敏原和用餐人数,为你规划整整一周的美味佳肴 +5. **🎲 不知道吃什么** - 选择困难症福音!根据人数直接推荐今日菜单,再也不用纠结了 + +## 🚀 快速上手 + +### 📋 先决条件 + +- Node.js 16.0.0+ 🟢 +- npm 或 yarn 📦 + +### 💻 安装步骤 + +1. 克隆美食仓库 + +```bash +git clone https://github.com/worryzyy/howtocook-mcp.git +cd howtocook-mcp +``` + +2. 安装依赖(就像准备食材一样简单!) + +```bash +npm install +``` + +3. 编译代码(烹饪过程...) + +```bash +npm run build +``` + +### 🎯 命令行参数 + +服务器支持以下命令行参数: + +- `--transport ` - 选择传输方式(默认为 stdio) +- `--port ` - 使用 http 或 sse 传输时的监听端口(默认为 3000) + +示例:使用 http 传输并监听 8080 端口 + +```bash +node build/index.js --transport http --port 8080 +``` + +## 🍽️ 开始使用 + +### 🔥 启动服务器 + +```bash +npm start +``` + +### 🔧 配置 MCP 客户端 + +#### 推荐使用 Cursor 快速体验(两种方式) + +1. 使用 npm 包:请先运行 `npm i -g howtocook-mcp` ,否则会出现 `Failed to create client` + +然后在 Cursor 设置中添加 MCP 服务器配置: + +```json +{ + "mcpServers": { + "howtocook-mcp": { + "command": "npx", + "args": ["-y", "howtocook-mcp"] + } + } +} +``` + +2. 如果是克隆仓库本地运行,请使用如下配置 + +```json +{ + "mcpServers": { + "howtocook-mcp": { + "command": "node", + "args": ["youpath\\howtocook-mcp\\build\\index.js"] + } + } +} +``` + +#### 其他 MCP 客户端 + +对于其他支持 MCP 协议的客户端,请参考各自的文档进行配置,通常需要指定: + +- 服务器名称: `howtocook-mcp` +- 命令: `npx -y howtocook-mcp` + +3. 重启客户端,让美食魔法生效 ✨ + +## 🧙‍♂️ 菜单魔法使用指南 + +以下是在各种 MCP 客户端中使用的示例提示语: + +### 1. 📚 查询全部菜谱 + +无需参数,直接召唤美食全书! + +``` +请使用howtocook的MCP服务查询所有菜谱 +``` + +### 2. 🔍 根据分类查询菜谱 + +``` +请使用howtocook的MCP服务查询水产类的菜谱 +``` + +参数: + +- `category`: 菜谱分类(水产、早餐、荤菜、主食等) + +### 3. 🧩 智能推荐一周菜谱 + +``` +请使用howtocook的MCP服务为3人推荐一周菜谱,我们家不吃香菜,对虾过敏 +``` + +参数: + +- `allergies`: 过敏原列表,如 ["大蒜", "虾"] +- `avoidItems`: 忌口食材,如 ["葱", "姜"] +- `peopleCount`: 用餐人数 (1-10) + +### 4. 🎲 今天吃什么? + +``` +请使用howtocook的MCP服务为4人晚餐推荐菜单 +``` + +参数: + +- `peopleCount`: 用餐人数 (1-10) + +## 📝 小贴士 + +- 该包已发布至 npm,可直接通过`npm install -g howtocook-mcp`全局安装 +- 本服务兼容所有支持 MCP 协议的 AI 助手和应用 +- 首次使用时,AI 可能需要一点时间来熟悉如何使用这些工具(就像烧热锅一样) + +## 🤝 贡献 + +欢迎 Fork 和 Pull Request,让我们一起完善这个美食助手! + +## 📄 许可 + +MIT License - 随意使用,就像分享美食配方一样慷慨! + +--- + +> 🍴 美食即将开始,胃口准备好了吗? + +## 写在最后 + +平时关注 MCP 比较多,特意新建了一个 MCP 的群聊,欢迎各位大佬加群讨论更多 MCP 的话题 + +
+MCP群聊 +
+ + +或者直接加作者 VX 进群:`worry3stone`, 请注明`MCP Exchange`,否则会被忽略哦 diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README_EN.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README_EN.md new file mode 100644 index 00000000..d52436cf --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README_EN.md @@ -0,0 +1,228 @@ +# 🍳 HowToCook-MCP Server 🥘 -- Plan Your Weekly Meals, No More Daily Struggles + +English | [简体中文](./README.md) + +
+ +CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne. + +[Best Asian CDN, Edge, and Secure Solutions - Tencent EdgeOne](https://edgeone.ai/zh?from=github) + + + +
+ +> Turn your AI assistant into a personal chef that helps plan your daily meals! + +An MCP (Model Context Protocol) server based on [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook), allowing AI assistants to recommend recipes, plan meals, and solve the age-old question of "what should I eat today?" + +Data Source: [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ Don't forget to star the repo if you haven't already! + +🎉 **Want to use MCP right away? Try it now** [https://howtocookmcp.weilei.site/](https://howtocookmcp.weilei.site/) + +🎉 **At the same time, we also provide DXT (Desktop Extensions) for everyone to experience, one-click installation to Claude Desktop** + +As follows: Please make sure you have installed the latest version of Claude Desktop. The current MCP DXT file has been uploaded to the code library. You can download it yourself or fork this repository to build it yourself + +![DXT](./public/dxt.png) + +![DXT](./public/dxt2.png) + +![DXT](./public/dxt3.png) + +How to package local development into DXT? + +1. Run `npm install -g @anthropic-ai/dxt` + +2. In the folder containing the local MCP server, run `dxt init`. That is, the root directory of your MCP. This command will guide you to create `manifest.json` + +3. Run `dxt pack` to create a dxt file + +Now, any application that supports DXT can run your local MCP server. For example, opening the file with Claude for macOS and Windows will display the installation dialog + +For more information, see: [anthropics/dxt](https://github.com/anthropics/dxt) + +## 📸 Preview + +![Feature Preview 1](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/1.png) +![Feature Preview 2](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/2.png) + +## 🔌 Supported MCP Clients + +This server works with all AI assistants and clients that support the MCP protocol, including but not limited to: + +- 🤖 Claude Desktop App +- 📝 Cursor +- 💼 Other MCP-compatible clients + +## ✨ Delicious Features + +This MCP server provides the following culinary tools: + +1. **📚 Query All Recipes** - Access all available recipe data, your complete cooking encyclopedia -- Use with caution due to large context size +2. **🔍 Query Recipes by Category** - Filter recipes by category: seafood, breakfast, meat dishes, staple foods, and more! +3. **🧩 Smart Meal Planning** - Get a full week's meal plan based on dietary restrictions, allergies, and number of diners +4. **🎲 Don't Know What to Eat?** - Perfect for the indecisive! Get instant menu recommendations based on party size +5. **🔎 Query Specific Recipe** - Search for specific recipes by name or ID, supports both exact and fuzzy matching to save tokens + +## 🚀 Quick Start + +### 📋 Prerequisites + +- Node.js 16.0.0+ 🟢 +- npm or yarn 📦 + +### 💻 Installation + +1. Clone the repository + +```bash +git clone https://github.com/worryzyy/howtocook-mcp.git +cd howtocook-mcp +``` + +2. Install dependencies (as simple as preparing ingredients!) + +```bash +npm install +``` + +3. Build the code (the cooking process...) + +```bash +npm run build +``` + +### 🎯 CLI Arguments + +The server accepts the following command-line arguments: + +- `--transport ` - Transport to use (stdio by default) +- `--port ` - Port to listen on when using http or sse transport (default 3000) + +Example with http transport and port 8080: + +```bash +node build/index.js --transport http --port 8080 +``` + +## ��️ Getting Started + +### 🔥 Start the Server + +```bash +npm start +``` + +### 🔧 Configure MCP Clients + +#### It is recommended to use Cursor for quick experience (two methods)Cursor Configuration + +1. Using npm package: Please run `npm i -g howtocook-mcp` first, otherwise `Failed to create client` will appear + +Then add the MCP server configuration in Cursor settings: + +```json +{ + "mcpServers": { + "howtocook-mcp": { + "command": "npx", + "args": ["-y", "howtocook-mcp"] + } + } +} +``` + +2. If running from a local cloned repository, use this configuration: + +```json +{ + "mcpServers": { + "howtocook-mcp": { + "command": "node", + "args": ["yourpath\\howtocook-mcp\\build\\index.js"] + } + } +} +``` + +#### Other MCP Clients + +For other clients supporting the MCP protocol, refer to their respective documentation. Generally, you'll need to specify: + +- Server name: `howtocook-mcp` +- Command: `npx -y howtocook-mcp` + +3. Restart the client to activate the culinary magic ✨ + +## 🧙‍♂️ Culinary Magic Usage Guide + +Here are example prompts for using these tools in MCP clients: + +### 1. 📚 Query All Recipes + +No parameters needed, just summon the culinary encyclopedia! + +``` +Please use the howtocook MCP service to query all recipes +``` + +### 2. 🔍 Query Recipes by Category + +``` +Please use the howtocook MCP service to query seafood recipes +``` + +Parameters: + +- `category`: Recipe category (seafood, breakfast, meat dishes, staple foods, etc.) + +### 3. 🧩 Smart Meal Planning + +``` +Please use the howtocook MCP service to recommend a weekly meal plan for 3 people. We don't eat cilantro and are allergic to shrimp. +``` + +Parameters: + +- `allergies`: List of allergens, e.g., ["garlic", "shrimp"] +- `avoidItems`: Dietary restrictions, e.g., ["green onion", "ginger"] +- `peopleCount`: Number of diners (1-10) + +### 4. 🎲 What to Eat Today? + +``` +Please use the howtocook MCP service to recommend a dinner menu for 4 people +``` + +Parameters: + +- `peopleCount`: Number of diners (1-10) + +### 5. 🔎 Query Specific Recipe + +``` +Please use the howtocook MCP service to query the recipe for "Kung Pao Chicken" +``` + +Parameters: + +- `recipeId`: Recipe name or ID to search for + +## 📝 Tips + +- This package is published on npm and can be installed globally via `npm install -g howtocook-mcp` +- Compatible with all AI assistants and applications that support the MCP protocol +- On first use, AI may need some time to familiarize itself with these tools (like preheating an oven) + +## 🤝 Contributing + +Forks and Pull Requests are welcome! Let's improve this culinary assistant together! + +## 📄 License + +MIT License - Feel free to use, just like sharing your favorite recipes! + +--- + +> 🍴 The feast is about to begin, is your appetite ready? diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/manifest.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/manifest.json new file mode 100644 index 00000000..5f0534cf --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/manifest.json @@ -0,0 +1,51 @@ +{ + "dxt_version": "0.1", + "name": "howtocook-mcp", + "version": "0.1.1", + "description": "MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭", + "author": { + "name": "worry", + "email": "weileihhh@gmail.com", + "url": "https://github.com/worryzyy" + }, + "homepage": "https://howtocookmcp.weilei.site", + "documentation": "https://github.com/worryzyy/HowToCook-mcp/blob/master/README.md", + "server": { + "type": "node", + "entry_point": "build/index.js", + "mcp_config": { + "command": "node", + "args": [ + "${__dirname}/build/index.js" + ], + "env": {} + } + }, + "tools": [ + { + "name": "mcp_howtocook_getAllRecipes", + "description": "获取所有菜谱" + }, + { + "name": "mcp_howtocook_getRecipeById", + "description": "根据菜谱名称或ID查询指定菜谱的完整详情,包括食材、步骤等" + }, + { + "name": "mcp_howtocook_getRecipesByCategory", + "description": "根据分类查询菜谱,菜谱分类名称,如水产、早餐、荤菜、主食等" + }, + { + "name": "mcp_howtocook_recommendMeals", + "description": "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单" + }, + { + "name": "mcp_howtocook_whatToEat", + "description": "不知道吃什么?根据人数直接推荐适合的菜品组合" + } + ], + "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/worryzyy/HowToCook-mcp" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/package.json new file mode 100644 index 00000000..32b8be97 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/package.json @@ -0,0 +1,56 @@ +{ + "name": "howtocook-mcp", + "version": "0.1.1", + "type": "module", + "main": "build/index.js", + "bin": { + "howtocook-mcp": "./build/index.js" + }, + "files": [ + "build", + "README.md" + ], + "scripts": { + "build": "tsc", + "prepare": "npm run build", + "start": "node build/index.js", + "start:stdio": "node build/index.js --transport stdio", + "start:http": "node build/index.js --transport http", + "start:sse": "node build/index.js --transport sse", + "dev": "tsc && node build/index.js", + "dev:stdio": "tsc && node build/index.js --transport stdio", + "dev:http": "tsc && node build/index.js --transport http", + "dev:sse": "tsc && node build/index.js --transport sse", + "prepublishOnly": "npm run build", + "publish:npm": "npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish" + }, + "keywords": [ + "howtocook", + "mcp", + "server", + "recipe", + "food", + "cook" + ], + "author": "worry", + "license": "ISC", + "description": "MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.9.0", + "@types/express": "^5.0.3", + "commander": "^14.0.0", + "express": "^5.1.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "typescript": "^5.3.3" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt.png new file mode 100644 index 00000000..aa7c52a8 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt2.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt2.png new file mode 100644 index 00000000..054d85c0 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt2.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt3.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt3.png new file mode 100644 index 00000000..58e16a8a Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt3.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/edgeone.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/edgeone.png new file mode 100644 index 00000000..576d9154 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/edgeone.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/wechat.jpg b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/wechat.jpg new file mode 100644 index 00000000..2f554cef Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/wechat.jpg differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/all_recipes.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/all_recipes.json new file mode 100644 index 00000000..ad3dece9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/all_recipes.json @@ -0,0 +1,52878 @@ +[ + { + "id": "dishes-aquatic-咖喱炒蟹", + "name": "咖喱炒蟹的做法", + "description": "# 咖喱炒蟹的做法\n\n第一次吃咖喱炒蟹是在泰国的建兴酒家中餐厅,爆肉的螃蟹挂满有蟹黄味道的咖喱,味道真的绝,喜欢吃海鲜的程序员绝对不能错过。操作简单,对沿海的程序员非常友好。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/咖喱炒蟹.md", + "image_path": null, + "images": [], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "青蟹(别称:肉蟹)", + "quantity": null, + "unit": null, + "text_quantity": "- 青蟹(别称:肉蟹)", + "notes": "量未指定" + }, + { + "name": "咖喱块(推介乐惠蟹黄咖喱)", + "quantity": null, + "unit": null, + "text_quantity": "- 咖喱块(推介乐惠蟹黄咖喱)", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "椰浆", + "quantity": null, + "unit": null, + "text_quantity": "- 椰浆", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "生粉(别称:淀粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉(别称:淀粉)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "肉蟹", + "quantity": null, + "unit": null, + "text_quantity": "- 肉蟹 1 只(大约 300g) * 份数", + "notes": "量未指定" + }, + { + "name": "咖喱块", + "quantity": null, + "unit": null, + "text_quantity": "- 咖喱块 15g(一小块)*份数", + "notes": "量未指定" + }, + { + "name": "椰浆", + "quantity": null, + "unit": null, + "text_quantity": "- 椰浆 100ml*份数", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个 *份数", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 200g *份数", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 5 瓣 *份数", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "肉蟹掀盖后对半砍开,蟹钳用刀背轻轻拍裂,切口和蟹钳蘸一下生粉,不要太多。撒 5g 生粉到蟹盖中,盖住蟹黄,备用" + }, + { + "step": 2, + "description": "洋葱切成洋葱碎,备用" + }, + { + "step": 3, + "description": "大蒜切碎,备用" + }, + { + "step": 4, + "description": "烧一壶开水,备用" + }, + { + "step": 5, + "description": "起锅烧油,倒入约 20ml 食用油,等待 10 秒让油温升高" + }, + { + "step": 6, + "description": "将螃蟹切口朝下,轻轻放入锅中,煎 20 秒,这一步主要是封住蟹黄,蟹肉。然后翻面,每面煎 10 秒。煎完将螃蟹取出备用" + }, + { + "step": 7, + "description": "将螃蟹盖放入锅中,使用勺子舀起锅中热油泼到蟹盖中,煎封住蟹盖中的蟹黄,煎 20 秒后取出备用" + }, + { + "step": 8, + "description": "不用刷锅,再倒入 10ml 食用油,大火让油温升高至轻微冒烟,将大蒜末,洋葱碎倒入,炒 10 秒钟" + }, + { + "step": 9, + "description": "将咖喱块放入锅中炒化(10 秒),放入煎好的螃蟹,翻炒均匀" + }, + { + "step": 10, + "description": "倒入开水 300ml,焖煮 3 分钟。" + }, + { + "step": 11, + "description": "焖煮完后,倒入椰浆和蛋清,关火,关火后不断翻炒,一直到酱汁变浓稠。" + }, + { + "step": 12, + "description": "出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。", + "做法参考:[十几年澳门厨房佬教学挂汁的咖喱蟹怎么做](https://www.bilibili.com/video/BV1Nq4y1W7K9)" + ] + }, + { + "id": "dishes-aquatic-微波葱姜黑鳕鱼", + "name": "微波葱姜黑鳕鱼的做法", + "description": "# 微波葱姜黑鳕鱼的做法\n\n这道菜改编自西雅图 Veil 餐厅主厨 Johnny Zhu 的母亲 Margaret Lu 的菜谱。卢女士原菜谱是使用罗非鱼来做这道菜,Johnny 改为鳕鱼,但也可以用大比目鱼鱼排,或者海鲈鱼、鳟鱼等。每种鱼的密度有差别,烹饪时间要做微调。\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/微波葱姜黑鳕鱼.md", + "image_path": null, + "images": [], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "黑鳕鱼,带皮", + "quantity": null, + "unit": null, + "text_quantity": "- 黑鳕鱼,带皮", + "notes": "量未指定" + }, + { + "name": "青葱", + "quantity": null, + "unit": null, + "text_quantity": "- 青葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "芝麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油", + "notes": "量未指定" + }, + { + "name": "花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油", + "notes": "量未指定" + }, + { + "name": "密封袋", + "quantity": null, + "unit": null, + "text_quantity": "- 密封袋", + "notes": "量未指定" + }, + { + "name": "黑鳕鱼,带皮,2 片,450g(本菜谱主角,所有调料可根据鳕鱼的实际重量进行比例调整)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑鳕鱼,带皮,2 片,450g(本菜谱主角,所有调料可根据鳕鱼的实际重量进行比例调整)", + "notes": "量未指定" + }, + { + "name": "青葱,葱白,25g。", + "quantity": null, + "unit": null, + "text_quantity": "- 青葱,葱白,25g。", + "notes": "量未指定" + }, + { + "name": "青葱,葱绿,10g。", + "quantity": null, + "unit": null, + "text_quantity": "- 青葱,葱绿,10g。", + "notes": "量未指定" + }, + { + "name": "姜,13g。", + "quantity": null, + "unit": null, + "text_quantity": "- 姜,13g。", + "notes": "量未指定" + }, + { + "name": "料酒,5mL。", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒,5mL。", + "notes": "量未指定" + }, + { + "name": "酱油,25mL。", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油,25mL。", + "notes": "量未指定" + }, + { + "name": "芝麻油,2mL。", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油,2mL。", + "notes": "量未指定" + }, + { + "name": "花生油,50mL。", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油,50mL。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鱼片分别放入密封袋,鱼皮向下放在盘子中。" + }, + { + "step": 2, + "description": "取葱白切丝 25g,姜去皮后切丝,10g,混合在一起后分成两半,分别放在袋内鱼片上。" + }, + { + "step": 3, + "description": "每个袋子倒入 2.5mL 料酒。" + }, + { + "step": 4, + "description": "封好密封袋,放入微波炉中,中火(800 瓦)微波至*不透明且容易散开*时(约 3.5-5 分钟),从袋中取出鱼片。" + }, + { + "step": 5, + "description": "去除青葱和姜。" + }, + { + "step": 6, + "description": "取酱油 25mL,芝麻油 2mL,混合均匀后平均淋在两片鱼片上。" + }, + { + "step": 7, + "description": "取葱绿切细丝 10g,姜去皮后切丝 3g,混合后分成两份撒在鱼片上。" + }, + { + "step": 8, + "description": "取花生油 50mL,在小锅中加热至 190℃。" + }, + { + "step": 9, + "description": "将热油淋到放油葱绿的鱼片上,立刻上桌。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-水煮鱼", + "name": "水煮鱼的做法", + "description": "# 水煮鱼的做法\n\n水煮鱼是一道做法中等难度的硬菜。巴沙鱼富含优质蛋白且脂肪含量低,配合各种时令蔬菜十分营养健康。初学者一般需要 2 小时即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/水煮鱼.md", + "image_path": null, + "images": [], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "巴沙鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 巴沙鱼", + "notes": "量未指定" + }, + { + "name": "蔬菜(比如土豆片/豆芽/花菜/生菜/……)", + "quantity": null, + "unit": null, + "text_quantity": "- 蔬菜(比如土豆片/豆芽/花菜/生菜/……)", + "notes": "量未指定" + }, + { + "name": "红油豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 红油豆瓣酱", + "notes": "量未指定" + }, + { + "name": "藤椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 藤椒油", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "量杯", + "quantity": null, + "unit": null, + "text_quantity": "- 量杯", + "notes": "量未指定" + }, + { + "name": "厨房秤(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 厨房秤(可选)", + "notes": "量未指定" + }, + { + "name": "大不锈钢碗", + "quantity": null, + "unit": null, + "text_quantity": "- 大不锈钢碗", + "notes": "量未指定" + }, + { + "name": "巴沙鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 巴沙鱼 500g", + "notes": "量未指定" + }, + { + "name": "蔬菜(比如土豆片/豆芽/花菜/生菜/……) 可有不同搭配,推荐合计重量", + "quantity": null, + "unit": null, + "text_quantity": "- 蔬菜(比如土豆片/豆芽/花菜/生菜/……) 可有不同搭配,推荐合计重量 300g 至 500g", + "notes": "量未指定" + }, + { + "name": "红油豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 红油豆瓣酱 40g (不怕辣想多加红油就多加 10 至 20g)", + "notes": "量未指定" + }, + { + "name": "豆豉", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉 10g (可选)", + "notes": "量未指定" + }, + { + "name": "藤椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 藤椒油 10ml", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油 25ml", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉 3g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 2 瓣", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5g", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "准备:巴沙鱼若是从冷冻柜里取出,需要放室温自然解冻 5 小时再做切片处理。" + }, + { + "step": 2, + "description": "切片:巴沙鱼撇成薄片,约 5cm 长,3cm 宽。" + }, + { + "step": 3, + "description": "[腌制](../../tips/learn/学习腌.md):将切好片的巴沙鱼放入大不锈钢碗中" + }, + { + "step": 4, + "description": "加入 30g 豆瓣酱,3g 盐,10ml 藤椒油,3g 白胡椒粉" + }, + { + "step": 5, + "description": "用手抓匀后加入 5ml 菜籽油收尾封住口味" + }, + { + "step": 6, + "description": "常温静置至少 30 分钟入味。" + }, + { + "step": 7, + "description": "备菜:大蒜切成蒜末。以 300g 花菜,200g 生菜为例,将花菜与生菜洗净。" + }, + { + "step": 8, + "description": "焯水与炒菜:花菜[开水锅焯水](../../tips/learn/学习焯水.md)备用;将生菜洗净晾干,炒熟备用(不用放油)。" + }, + { + "step": 9, + "description": "炒豆瓣酱:热锅冷油(菜籽油 20ml),加入 10g 豆瓣酱,10g 豆豉(可选),加入蒜末,**中火**慢炒。" + }, + { + "step": 10, + "description": "汆鱼片:加入 150ml 热水,水很快开后加入腌制好的鱼片,轻轻翻动让鱼片在水中散开,加入 2g 盐和 2g 糖调味(此时可根据个人口味调整盐的用量)。水再次沸腾后即可盛盘。" + }, + { + "step": 11, + "description": "盛盘:先将熟的蔬菜盛至大碗中,然后将热的鱼片盛在蔬菜上面,浇上锅中剩余热汤即可!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-清蒸生蚝", + "name": "清蒸生蚝的做法", + "description": "# 清蒸生蚝的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/清蒸生蚝.md", + "image_path": null, + "images": [], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "生蚝", + "quantity": null, + "unit": null, + "text_quantity": "- 生蚝", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "刷子", + "quantity": null, + "unit": null, + "text_quantity": "- 刷子", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 1 升", + "notes": "量未指定" + }, + { + "name": "生蚝", + "quantity": null, + "unit": null, + "text_quantity": "- 生蚝 6 个", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 3 颗", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 6 瓣", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 1 小块", + "notes": "量未指定" + }, + { + "name": "酱油 每个生蚝", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 每个生蚝 1 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将生蚝用刷子刷干净(没有刷子用牙刷)。" + }, + { + "step": 2, + "description": "蒸锅中放水,将蒸屉放上之后,将 6 个生蚝平铺在蒸屉,使用 50%功率,蒸 3 分钟。" + }, + { + "step": 3, + "description": "用右手拿着湿抹布掀开烫锅盖,将每个生蚝的外壳掀开一半去掉,生蚝的凸面向下,平面向上,每个放 1 根姜丝,10g 蒜末放到生蚝上。" + }, + { + "step": 4, + "description": "关上烫锅盖,100%功率蒸 3.5 分钟。" + }, + { + "step": 5, + "description": "停火,用右手拿着抹布掀开烫锅盖,每个放 5ml 酱油。" + }, + { + "step": 6, + "description": "盛盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-红烧鱼", + "name": "红烧鱼的做法", + "description": "# 红烧鱼的做法\n\n- **WARNING** 如果没有使用过菜刀剁过肉类食物,那么并不推荐使用该菜单!!!\n- 在操作中,锋利的菜刀可能会划伤手指,请一定要小心。\n\n- 此做法代表通用红烧鱼做法,材料分为必备和可添加~\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/红烧鱼.md", + "image_path": null, + "images": [], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "姜、蒜瓣、干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 姜、蒜瓣、干辣椒", + "notes": "量未指定" + }, + { + "name": "油、盐、料酒、醋、酱油、白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 油、盐、料酒、醋、酱油、白砂糖", + "notes": "量未指定" + }, + { + "name": "鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 鱼", + "notes": "量未指定" + }, + { + "name": "鱼 建议新手以一条中等大小的鲫鱼上手,提前划好花刀,方便成熟(不然鱼背上容易夹生)", + "quantity": null, + "unit": null, + "text_quantity": "- 鱼 建议新手以一条中等大小的鲫鱼上手,提前划好花刀,方便成熟(不然鱼背上容易夹生)", + "notes": "量未指定" + }, + { + "name": "姜丝,以正常老姜切", + "quantity": null, + "unit": null, + "text_quantity": "- 姜丝,以正常老姜切 2-3 片,然后片丝即可", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 3-4 个,拍碎或者切碎或者切片", + "notes": "量未指定" + }, + { + "name": "干辣椒(依照个人口味)2-3 个,切碎", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒(依照个人口味)2-3 个,切碎", + "notes": "量未指定" + }, + { + "name": "香菜按照个人口味", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜按照个人口味", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 10g,如果辣椒辣度高,建议多一点", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 5ml", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 5ml", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 10g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1-2 根,正常就撒葱花", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 1-2,不放也可以,过来人的经验,最多放 2 个,不然辣度过高要菊花残。", + "notes": "量未指定" + }, + { + "name": "味精,看个人口味,不要放多,5g 即可。", + "quantity": null, + "unit": null, + "text_quantity": "- 味精,看个人口味,不要放多,5g 即可。", + "notes": "量未指定" + }, + { + "name": "蚝油,5g 即可,和味精一个道理", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油,5g 即可,和味精一个道理", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "姜蒜准备好,切碎" + }, + { + "step": 2, + "description": "干辣椒切碎,和姜蒜一起" + }, + { + "step": 3, + "description": "加入 30-50ml 油,等待锅热..." + }, + { + "step": 4, + "description": "放入**擦干水分的鱼**(不想被热油溅一身的话),然后晃动锅,用热油煎鱼,注意这过程一定要小火" + }, + { + "step": 5, + "description": "将鱼翻面,重复上面油煎过程" + }, + { + "step": 6, + "description": "放入姜蒜辣椒,翻炒出香味" + }, + { + "step": 7, + "description": "倒入料酒,稍微多一点,此过程注意安全,会起大量油烟" + }, + { + "step": 8, + "description": "倒入醋(喜欢醋可以多放一点)" + }, + { + "step": 9, + "description": "然后放入白砂糖,酱油(老抽)" + }, + { + "step": 10, + "description": "加入冷水,以刚好淹没鱼身为宜,然后调成中火,盖上锅盖,大概 1 分钟后将鱼翻身,继续盖上锅盖" + }, + { + "step": 11, + "description": "3-4 分钟后,加入盐、小米椒、蚝油(味精、鸡精等),然后继续盖上锅盖,后续继续要翻身" + }, + { + "step": 12, + "description": "当锅内汤汁收汁到鱼的脊背线上的鱼鳍下面一点点的时候(或者汤汁不多的时候),转小火,加入香菜,葱花,然后盖上锅盖 20 秒,关火" + }, + { + "step": 13, + "description": "起锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-红烧鱼头", + "name": "红烧鱼头的做法", + "description": "# 红烧鱼头的做法\n\n- **WARNING** 如果没有使用过菜刀剁过肉类食物,那么并不推荐使用该菜单!!!\n- 在操作中,锋利的菜刀可能会划伤手指,请一定要小心。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/红烧鱼头.md", + "image_path": null, + "images": [], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "大葱、姜、大蒜、香菜、美人椒", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱、姜、大蒜、香菜、美人椒", + "notes": "量未指定" + }, + { + "name": "油、盐、鸡精、生抽、老抽、陈醋、黑胡椒粉、料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 油、盐、鸡精、生抽、老抽、陈醋、黑胡椒粉、料酒", + "notes": "量未指定" + }, + { + "name": "八角、干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 八角、干辣椒", + "notes": "量未指定" + }, + { + "name": "鱼头一个", + "quantity": null, + "unit": null, + "text_quantity": "- 鱼头一个", + "notes": "量未指定" + }, + { + "name": "注:市场直接贩卖的鱼头一般分为两种:白鲢、花鲢。前者价格便宜,后者价格略贵,但口感也更佳!", + "quantity": null, + "unit": null, + "text_quantity": "- 注:市场直接贩卖的鱼头一般分为两种:白鲢、花鲢。前者价格便宜,后者价格略贵,但口感也更佳!", + "notes": "量未指定" + }, + { + "name": "鱼头一个", + "quantity": null, + "unit": null, + "text_quantity": "- 鱼头一个", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 200g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 80g", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 3-4 个", + "notes": "量未指定" + }, + { + "name": "美人椒", + "quantity": null, + "unit": null, + "text_quantity": "- 美人椒 1/4 个", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 4 棵", + "notes": "量未指定" + }, + { + "name": "八角两个,干辣椒五个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角两个,干辣椒五个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "葱、姜、蒜、香菜、美人椒分别清洗干净。" + }, + { + "step": 2, + "description": "干辣椒与八角稍微冲洗即可。" + }, + { + "step": 3, + "description": "大葱切两半。后半段大葱(葱白处)切段,每段长度约 4cm。前半段(葱叶处)先切段,再将每段劈为四瓣。" + }, + { + "step": 4, + "description": "姜切片,每片厚度约 3mm。" + }, + { + "step": 5, + "description": "大蒜拍碎。" + }, + { + "step": 6, + "description": "拿出两棵香菜去根,切为 1.5cm 香菜碎。" + }, + { + "step": 7, + "description": "将美人椒切为厚度为 3mm 的辣椒圈。" + }, + { + "step": 8, + "description": "干辣椒切四段。" + }, + { + "step": 9, + "description": "注:下文所述的鱼身是购买鱼头时所附带的鱼肉。" + }, + { + "step": 10, + "description": "将鱼头去鳞,清洗鱼头处未被清理干净的内脏。" + }, + { + "step": 11, + "description": "剁去鱼鳍、清理鱼鳃。" + }, + { + "step": 12, + "description": "将鱼头下巴与鱼身连接的地方剁开,鱼身剁块,鱼头剁成四/六瓣。" + }, + { + "step": 13, + "description": "注:鱼的处理很难用文字完全表述,可以搜索鱼头处理相关视频。" + }, + { + "step": 14, + "description": "将剁好的鱼头进行清洗,最好洗掉鱼块上滞留的血水。" + }, + { + "step": 15, + "description": "将清洗好的鱼块放入盆中,加入 5g 盐、10g 生抽、10g 料酒。放入葱(前半段切碎的那个)、1/3 姜片。将其拌匀,静置 1-2 小时。" + }, + { + "step": 16, + "description": "加入 30ml 油,等待锅热..." + }, + { + "step": 17, + "description": "油热,将锅关至小火" + }, + { + "step": 18, + "description": "如果不明白为何要这样做,请查看[学习炒与煎](../../tips/learn/学习炒与煎.md)中的翻炒辅料。" + }, + { + "step": 19, + "description": "放入姜片,慢慢翻炒,以姜片中的大部分汁水被炒出,以金黄色为准。" + }, + { + "step": 20, + "description": "放入葱段,翻炒至葱段略显发白。" + }, + { + "step": 21, + "description": "放入蒜碎、八角、干辣椒,翻炒 5 秒。" + }, + { + "step": 22, + "description": "将腌制好的鱼头倒入锅中,翻炒 2-3 分钟。" + }, + { + "step": 23, + "description": "倒入 500ml 清水,加入 2g 盐、3g 鸡精、5g 生抽、3g 老抽、5g 料酒、2g 黑胡椒粉、3g 陈醋。" + }, + { + "step": 24, + "description": "将两棵香菜放入锅中,盖上锅盖。" + }, + { + "step": 25, + "description": "调至大火,将水烧开。" + }, + { + "step": 26, + "description": "调至中火,慢焖入味。" + }, + { + "step": 27, + "description": "当汤汁减少一半时,打开锅盖。" + }, + { + "step": 28, + "description": "调至大火收汁,汤汁剩余 1/3 时,关火盛至小盆中。" + }, + { + "step": 29, + "description": "注:将锅中的汤汁均匀淋到鱼头上,盛盘时可以将锅中煮的香菜放入小盆底部,这样能让成品菜好看又好吃。" + }, + { + "step": 30, + "description": "将香菜放至已经盛出的鱼头上,把切好的美人椒圈放在香菜之上。" + }, + { + "step": 31, + "description": "色香味俱全的红烧鱼头出炉!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-红烧鲤鱼", + "name": "红烧鲤鱼的做法", + "description": "# 红烧鲤鱼的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/红烧鲤鱼.md", + "image_path": null, + "images": [], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "大葱、姜、大蒜、干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱、姜、大蒜、干辣椒", + "notes": "量未指定" + }, + { + "name": "油、盐、生抽、老抽、陈醋、蚝油、料酒、白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 油、盐、生抽、老抽、陈醋、蚝油、料酒、白糖", + "notes": "量未指定" + }, + { + "name": "鲤鱼、五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鲤鱼、五花肉", + "notes": "量未指定" + }, + { + "name": "鲤鱼 (大约", + "quantity": null, + "unit": null, + "text_quantity": "- 鲤鱼 (大约 2 斤)", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 100g", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 200g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 80g", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 3-4 个", + "notes": "量未指定" + }, + { + "name": "干辣椒两个", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒两个", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 50g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "葱、姜、蒜、干辣椒分别清洗干净。" + }, + { + "step": 2, + "description": "葱白处切段,每段长度约 4cm,再将每段劈为四瓣。" + }, + { + "step": 3, + "description": "姜切片,每片厚度约 3mm。" + }, + { + "step": 4, + "description": "一个大蒜拍碎切末,其余蒜切为二瓣。" + }, + { + "step": 5, + "description": "干辣椒切四段。" + }, + { + "step": 6, + "description": "五花肉切片,约 4cm*4cm。" + }, + { + "step": 7, + "description": "清洗鱼。" + }, + { + "step": 8, + "description": "鱼背肉厚处拉几道斜口,方便入味" + }, + { + "step": 9, + "description": "锅里多倒点油,烧至 7 成热(刚刚开始冒烟),下入鱼炸 1 分钟至鱼皮稍稍变硬捞出备用(注意不要一下锅就拨弄鱼,等炸一会再拨弄、翻面),炸鱼的油倒出,锅里留一点底油" + }, + { + "step": 10, + "description": "将锅里底油烧热,下入五花肉,煸出香味。" + }, + { + "step": 11, + "description": "放入干辣椒、葱、姜、蒜瓣,翻炒 1 分钟。" + }, + { + "step": 12, + "description": "将炸好的鱼倒入锅中。" + }, + { + "step": 13, + "description": "沿锅边倒入" + }, + { + "step": 14, + "description": "调至中火,将水烧开。" + }, + { + "step": 15, + "description": "调至小火,慢焖入味。" + }, + { + "step": 16, + "description": "15 分钟 后,打开锅盖,挑出锅里的葱、姜、蒜、干辣椒。" + }, + { + "step": 17, + "description": "调至大火收汁,汤汁剩余 1/4 时,撒点蒜末,关火盛出。" + }, + { + "step": 18, + "description": "红烧鲤鱼出锅!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-小龙虾-小龙虾", + "name": "小龙虾的做法", + "description": "# 小龙虾的做法\n\n![成品](./成品.jpg)\n\n在家里做的小龙虾,肉质细嫩,鲜嫩多汁,干净卫生。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/小龙虾/小龙虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/小龙虾/成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/小龙虾/成品.jpg" + ], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "小龙虾", + "quantity": null, + "unit": null, + "text_quantity": "- 小龙虾", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "青花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "子弹头辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 子弹头辣椒", + "notes": "量未指定" + }, + { + "name": "葱姜蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜蒜", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣", + "notes": "量未指定" + }, + { + "name": "黄豆酱", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "小龙虾 =", + "quantity": null, + "unit": null, + "text_quantity": "- 小龙虾 = 2 斤", + "notes": "量未指定" + }, + { + "name": "油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 油 = 70 毫升(这是平时炒菜 3 倍量)", + "notes": "量未指定" + }, + { + "name": "香叶 = 两片", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 = 两片", + "notes": "量未指定" + }, + { + "name": "八角 = 一个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 = 一个", + "notes": "量未指定" + }, + { + "name": "桂皮 =", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 = 3 克", + "notes": "量未指定" + }, + { + "name": "青花椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒 = 10 克", + "notes": "量未指定" + }, + { + "name": "花椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 = 10 克", + "notes": "量未指定" + }, + { + "name": "子弹头辣椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 子弹头辣椒 = 5 克", + "notes": "量未指定" + }, + { + "name": "葱 = 一根大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 = 一根大葱", + "notes": "量未指定" + }, + { + "name": "姜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 = 30 克", + "notes": "量未指定" + }, + { + "name": "蒜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 = 7 瓣大蒜", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣 =", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣 = 30 克", + "notes": "量未指定" + }, + { + "name": "黄豆酱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱 = 30 克", + "notes": "量未指定" + }, + { + "name": "啤酒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒 = 500 毫升", + "notes": "量未指定" + }, + { + "name": "生抽 =", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 = 30 毫升", + "notes": "量未指定" + }, + { + "name": "盐 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = 10 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "小龙虾刷干净去虾线,葱切 2cm 葱段,姜蒜切末。" + }, + { + "step": 2, + "description": "烧油,油微热, 下香叶、八角、桂皮、青花椒、花椒、子弹头辣椒。" + }, + { + "step": 3, + "description": "香料出香气之后下锅葱姜蒜" + }, + { + "step": 4, + "description": "葱姜蒜爆香后,加入郫县豆瓣、黄豆酱,炒出红油。" + }, + { + "step": 5, + "description": "下小龙虾,翻炒至变色。" + }, + { + "step": 6, + "description": "加入啤酒,等啤酒烧开后加入生抽,盐。" + }, + { + "step": 7, + "description": "将小龙虾完全煮熟后出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-干煎阿根廷红虾-干煎阿根廷红虾", + "name": "干煎阿根廷红虾的做法", + "description": "# 干煎阿根廷红虾的做法\n\n![示例菜成品](./干煎阿根廷红虾.jpg)\n\n平常所见到虾,只有赴“汤”蹈“火”后,才能红!阿根廷虾很任性,一红就红一辈子!跟它住在北极的亲戚,北极虾一样,天生红。\n\n阿根廷红虾,之所以这么红,是因为它生活在深海中,使得它体内含有丰富的碘、磷及珍贵的虾青素等微量元素,能够增强人体免疫力,还对心脏活动具有重要调节作用,可以减少血液中的胆固醇含量。\n\n阿根廷红虾,不仅个大肥美,虾肉白如凝脂,细腻腴滑,口感鲜嫩,味道甜香浓郁,是虾类料理界的宠儿,看着真让人垂(chao)涎(ji)欲(xiang)滴(chi),快享受这大快朵颐的欢愉吧!\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/干煎阿根廷红虾/干煎阿根廷红虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/干煎阿根廷红虾/干煎阿根廷红虾.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/干煎阿根廷红虾/干煎阿根廷红虾.jpg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "阿根廷红虾(选用了速冻虾)", + "quantity": null, + "unit": null, + "text_quantity": "- 阿根廷红虾(选用了速冻虾)", + "notes": "量未指定" + }, + { + "name": "海盐(研磨装)", + "quantity": null, + "unit": null, + "text_quantity": "- 海盐(研磨装)", + "notes": "量未指定" + }, + { + "name": "黑胡椒(研磨装)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒(研磨装)", + "notes": "量未指定" + }, + { + "name": "白葡萄酒", + "quantity": null, + "unit": null, + "text_quantity": "- 白葡萄酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "阿根廷红虾", + "quantity": null, + "unit": null, + "text_quantity": "- 阿根廷红虾 2-3 只", + "notes": "量未指定" + }, + { + "name": "海盐", + "quantity": null, + "unit": null, + "text_quantity": "- 海盐 5g", + "notes": "量未指定" + }, + { + "name": "黑胡椒(研磨装)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒(研磨装)", + "notes": "量未指定" + }, + { + "name": "白葡萄酒", + "quantity": null, + "unit": null, + "text_quantity": "- 白葡萄酒 20ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 1ml", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 3 片", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 1 片", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 10g", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 10g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "阿根廷红虾解冻,最好是提前 1 天从速冻取出放到冷藏里自然解冻,能更好保持风味和口感。可买已经开背去虾线的,节省了不少时间" + }, + { + "step": 2, + "description": "解冻好的红虾洗净擦干备用,注意这里一定要沥干水分,赶时间可以用厨房用纸吸干水分" + }, + { + "step": 3, + "description": "生姜切片,洋葱切小方块,香菜洗干净后,叶茎分离,把香菜叶切碎,大蒜压碎切成小块碎末" + }, + { + "step": 4, + "description": "大火热锅,热锅后倒入两调羹橄榄油,等油温升高后,放入生姜片,洋葱块和香菜茎煸炒" + }, + { + "step": 5, + "description": "约 1 分钟后取出生姜,洋葱和香菜茎,弃用" + }, + { + "step": 6, + "description": "调中大火,放入红虾开始煎,注意所有虾需要单面都完整接触平底锅,煎约 2 分钟,同时给每只虾刷上一层油" + }, + { + "step": 7, + "description": "待底面虾壳有微微焦黄时翻面,并撒入大蒜碎末,轻微晃动平底锅使得受热均匀" + }, + { + "step": 8, + "description": "约 1 分钟后添加 20ml 白葡萄酒" + }, + { + "step": 9, + "description": "再煎 1 分钟后调中小火,均匀撒上一层盐和黑胡椒" + }, + { + "step": 10, + "description": "给每只虾滴上一滴生抽" + }, + { + "step": 11, + "description": "撒上香菜叶,装盘" + }, + { + "step": 12, + "description": "切好柠檬片,摆放到盘边即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-油焖大虾-油焖大虾", + "name": "油焖大虾的做法", + "description": "# 油焖大虾的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/油焖大虾/油焖大虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/油焖大虾/油焖大虾.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/油焖大虾/油焖大虾.jpg" + ], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "黑虎虾 or 明虾、", + "quantity": null, + "unit": null, + "text_quantity": "- 黑虎虾 or 明虾、", + "notes": "量未指定" + }, + { + "name": "葱、姜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱、姜", + "notes": "量未指定" + }, + { + "name": "料酒、盐、冰糖、植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒、盐、冰糖、植物油", + "notes": "量未指定" + }, + { + "name": "虾", + "quantity": null, + "unit": null, + "text_quantity": "- 虾 10 只", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 5g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 50g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 20g", + "notes": "量未指定" + }, + { + "name": "黄酒", + "quantity": null, + "unit": null, + "text_quantity": "- 黄酒 30g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 10g", + "notes": "量未指定" + }, + { + "name": "植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "剪虾枪到根上,虾须虾爪都剪掉,沙包挑掉,开背虾线挑出来,洗净备用" + }, + { + "step": 2, + "description": "炸料油" + }, + { + "step": 3, + "description": "下油,虾摆放整齐,两面变色后轻轻摁虾头" + }, + { + "step": 4, + "description": "大火烧开转小火盖盖子闷(中途不能再加汤水,不要开盖)" + }, + { + "step": 5, + "description": "皮亮虾弯就可以起锅,虾摆盘" + }, + { + "step": 6, + "description": "收汁(过滤后倒回锅里收浓,放葱油 ) 汤汁剩余 1/4 时。" + }, + { + "step": 7, + "description": "浇汁" + }, + { + "step": 8, + "description": "完成" + }, + { + "step": 9, + "description": "![成品](./油焖大虾.jpg)" + }, + { + "step": 10, + "description": "开吃✅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-混合烤鱼-烤鱼", + "name": "烤鱼的做法", + "description": "# 烤鱼的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/混合烤鱼/烤鱼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/混合烤鱼/烤鱼.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/混合烤鱼/烤鱼.jpg" + ], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "草鱼(农贸市场或者超市让店家杀掉,去除不要的器官)", + "quantity": null, + "unit": null, + "text_quantity": "- 草鱼(农贸市场或者超市让店家杀掉,去除不要的器官)", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "青花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒", + "notes": "量未指定" + }, + { + "name": "干辣椒段", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒段", + "notes": "量未指定" + }, + { + "name": "灯笼椒", + "quantity": null, + "unit": null, + "text_quantity": "- 灯笼椒", + "notes": "量未指定" + }, + { + "name": "火锅底料(随意)", + "quantity": null, + "unit": null, + "text_quantity": "- 火锅底料(随意)", + "notes": "量未指定" + }, + { + "name": "千张", + "quantity": null, + "unit": null, + "text_quantity": "- 千张", + "notes": "量未指定" + }, + { + "name": "绿豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 绿豆芽", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "芹菜段", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜段", + "notes": "量未指定" + }, + { + "name": "熟花生米", + "quantity": null, + "unit": null, + "text_quantity": "- 熟花生米", + "notes": "量未指定" + }, + { + "name": "白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻", + "notes": "量未指定" + }, + { + "name": "香菜(放更好吃,根据个人口味可放可不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜(放更好吃,根据个人口味可放可不放)", + "notes": "量未指定" + }, + { + "name": "草鱼 大约三斤", + "quantity": null, + "unit": null, + "text_quantity": "- 草鱼 大约三斤", + "notes": "量未指定" + }, + { + "name": "大葱 半根", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 半根", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 20ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10-15ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 5-10g", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉 5g-10g", + "notes": "量未指定" + }, + { + "name": "桂皮 一小片", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 一小片", + "notes": "量未指定" + }, + { + "name": "八角 两个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 两个", + "notes": "量未指定" + }, + { + "name": "大蒜粒 八个", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜粒 八个", + "notes": "量未指定" + }, + { + "name": "香叶 两张", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 两张", + "notes": "量未指定" + }, + { + "name": "青花椒 一小把", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒 一小把", + "notes": "量未指定" + }, + { + "name": "干辣椒段", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒段 10 个", + "notes": "量未指定" + }, + { + "name": "灯笼椒", + "quantity": null, + "unit": null, + "text_quantity": "- 灯笼椒 4 个", + "notes": "量未指定" + }, + { + "name": "芹菜段 两根", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜段 两根", + "notes": "量未指定" + }, + { + "name": "洋葱 半个", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 半个", + "notes": "量未指定" + }, + { + "name": "千张 一张", + "quantity": null, + "unit": null, + "text_quantity": "- 千张 一张", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "草鱼(一般 3 斤 )从背部切开,两面沿着鱼的背部往下划几刀,不要划到鱼肚皮,不然不易定型" + }, + { + "step": 2, + "description": "把鱼放到容器中,加入料酒,10g 白胡椒粉,5g 食盐抹匀腌制二十分钟入味。" + }, + { + "step": 3, + "description": "把半根大葱切成一块一块,大蒜粒中间切开,和八角香叶桂皮放在一个容器中" + }, + { + "step": 4, + "description": "干辣椒段中间一分为二切开并和灯笼椒装在一个容器中" + }, + { + "step": 5, + "description": "芹菜切小段" + }, + { + "step": 6, + "description": "豆芽焯水" + }, + { + "step": 7, + "description": "千张焯水切成丝" + }, + { + "step": 8, + "description": "洋葱切成丝。" + }, + { + "step": 9, + "description": "烤制鱼" + }, + { + "step": 10, + "description": "锅中撒上 20ml 食用油,等到油热后,把大葱大蒜八角香叶倒入炒香" + }, + { + "step": 11, + "description": "加上一包火锅底料的一半和 15-20g 豆瓣酱,炒出红油" + }, + { + "step": 12, + "description": "加入 5g 白糖,10g 食盐,5ml 生抽调味,倒入和食材齐平的清水煮开" + }, + { + "step": 13, + "description": "依次下入芹菜段,豆芽,千张丝,不用煮熟,稍微烫一下后铺上洋葱丝,放上烤鱼" + }, + { + "step": 14, + "description": "加入干辣椒,灯笼椒,青花椒" + }, + { + "step": 15, + "description": "另一个锅烧油,油热后浇在刚加入的辣椒上面激发出香味" + }, + { + "step": 16, + "description": "最后撒上熟花生米,葱花,白芝麻,香菜" + }, + { + "step": 17, + "description": "煮 5-6 分钟,美味即成。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-清蒸鲈鱼-清蒸鲈鱼", + "name": "清蒸鲈鱼的做法", + "description": "# 清蒸鲈鱼的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/清蒸鲈鱼/清蒸鲈鱼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/摆盘.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/摆盘.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/改刀.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/清蒸鲈鱼.jpg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "鲈鱼(害怕杀鱼的同学可以让店家帮忙杀)", + "quantity": null, + "unit": null, + "text_quantity": "- 鲈鱼(害怕杀鱼的同学可以让店家帮忙杀)", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "蒸鱼豉油", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸鱼豉油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "鲈鱼 一条", + "quantity": null, + "unit": null, + "text_quantity": "- 鲈鱼 一条", + "notes": "量未指定" + }, + { + "name": "香葱 三根", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 三根", + "notes": "量未指定" + }, + { + "name": "姜 一块", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 一块", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + }, + { + "name": "蒸鱼豉油", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸鱼豉油 10-15ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10-15ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 5-10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "姜切片切丝、香葱的葱白切段,葱绿切丝,切丝后放入冷水浸泡备用。" + }, + { + "step": 2, + "description": "鲈鱼处理好后洗净,用厨房纸擦干,两面分别划几刀,用盐洗掉鱼身的粘液,并用 10g 盐抹遍鱼身的内外,腌制 10 分钟以上。" + }, + { + "step": 3, + "description": "补充一个鲈鱼改刀和摆盘的方法,改刀后可以让鲈鱼立起来蒸,均匀受热,同时吃起来更加方便,无需翻面。" + }, + { + "step": 4, + "description": "![改刀](./改刀.jpg)" + }, + { + "step": 5, + "description": "![摆盘](./摆盘.jpg)" + }, + { + "step": 6, + "description": "鱼肚内塞上姜和葱白,鱼身也撒上姜和葱白,量为备用的一半。蒸鱼的碟子用筷子将鱼跟碟子隔开蒸" + }, + { + "step": 7, + "description": "水烧热感觉到水温后放进入鱼" + }, + { + "step": 8, + "description": "大火清蒸 10 分钟。" + }, + { + "step": 9, + "description": "蒸好的鱼,用干净的盘子装起来并去除身上姜蒜" + }, + { + "step": 10, + "description": "鱼身浇上 15ml 蒸鱼豉油" + }, + { + "step": 11, + "description": "鱼身重新撒上姜和葱丝,锅内加上 10ml 食用油并烧热,将食用油淋至鱼身即可出菜" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-白灼虾-白灼虾", + "name": "白灼虾的做法", + "description": "# 白灼虾的做法\n\n白灼虾非常适合程序员在沿海地区做,类似于清蒸鱼:简单容错、有营养、有满足感,甚至很好看。\n\n预估烹饪难度:★★", + "source_path": "dishes/aquatic/白灼虾/白灼虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/白灼虾/白灼虾.webp", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/白灼虾/白灼虾.webp" + ], + "category": "水产", + "difficulty": 2, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "活虾", + "quantity": null, + "unit": null, + "text_quantity": "- 活虾", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "虾", + "quantity": null, + "unit": null, + "text_quantity": "- 虾 250g * 份数(建议 1-2 人份)", + "notes": "量未指定" + }, + { + "name": "葱 一根", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 一根", + "notes": "量未指定" + }, + { + "name": "姜 一块", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 一块", + "notes": "量未指定" + }, + { + "name": "洋葱 一头", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 一头", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 5-8 瓣", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 20 ml", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 10-15ml", + "notes": "量未指定" + }, + { + "name": "芝麻 一把", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻 一把", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 10 ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 10 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "洋葱切小块,姜切片,平铺平底锅。" + }, + { + "step": 2, + "description": "活虾冲洗一下(去除虾线、剪刀减掉虾腿虾须子都是可选操作),控水,铺在平底锅的洋葱、姜片之上。" + }, + { + "step": 3, + "description": "锅内倒入料酒,盖上锅盖,中火 1 分钟,小火 5 分钟,关火 5 分钟。" + }, + { + "step": 4, + "description": "和上一步并行操作,制作蘸料:" + }, + { + "step": 5, + "description": "虾出锅,用干净的盘子装好。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-糖醋鲤鱼-糖醋鲤鱼", + "name": "糖醋鲤鱼的做法", + "description": "# 糖醋鲤鱼的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/糖醋鲤鱼/糖醋鲤鱼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/糖醋鲤鱼/成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/糖醋鲤鱼/成品.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/糖醋鲤鱼/腌制.jpg" + ], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "鲤鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 鲤鱼", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "香菜一颗", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜一颗", + "notes": "量未指定" + }, + { + "name": "盆(两个)", + "quantity": null, + "unit": null, + "text_quantity": "- 盆(两个)", + "notes": "量未指定" + }, + { + "name": "菜刀一个", + "quantity": null, + "unit": null, + "text_quantity": "- 菜刀一个", + "notes": "量未指定" + }, + { + "name": "笊篱一个、锅铲一个", + "quantity": null, + "unit": null, + "text_quantity": "- 笊篱一个、锅铲一个", + "notes": "量未指定" + }, + { + "name": "鲤鱼 = 约", + "quantity": null, + "unit": null, + "text_quantity": "- 鲤鱼 = 约 3 斤", + "notes": "量未指定" + }, + { + "name": "清水 =", + "quantity": null, + "unit": null, + "text_quantity": "- 清水 = 50g", + "notes": "量未指定" + }, + { + "name": "番茄酱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 = 40g", + "notes": "量未指定" + }, + { + "name": "白糖 =", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 = 20g", + "notes": "量未指定" + }, + { + "name": "白醋 =", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋 = 10g", + "notes": "量未指定" + }, + { + "name": "淀粉 =", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 = 10g", + "notes": "量未指定" + }, + { + "name": "盐 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = 30g", + "notes": "量未指定" + }, + { + "name": "大葱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 = 30g(约半颗)", + "notes": "量未指定" + }, + { + "name": "姜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 = 30g", + "notes": "量未指定" + }, + { + "name": "料酒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 = 25g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将鱼清洗干净,确保无鱼鳞等异物" + }, + { + "step": 2, + "description": "将鱼头朝左,鱼肚朝下,右手持刀。刀竖直切下 1cm,按紧鱼身往左片 3-4cm,再将鱼片中间轻轻划一刀" + }, + { + "step": 3, + "description": "将鱼放进盆里,然后将大姜切片,大葱切段(随便切切就行了,主要是需要去腥味)" + }, + { + "step": 4, + "description": "用吃奶的力气将大葱大姜里的汁水挤到盆中" + }, + { + "step": 5, + "description": "加入 20g 盐,25g 料酒,然后给鲤鱼搓个澡,涂抹均匀" + }, + { + "step": 6, + "description": "![腌制](./腌制.jpg)" + }, + { + "step": 7, + "description": "找个干净的盆,加入 100g 面粉、200g 淀粉、180g 水、5g 盐,用手将其搅拌均匀,面糊此时粘稠呈可拉丝状态,然后打入一个鸡蛋,再次搅匀" + }, + { + "step": 8, + "description": "等待 30 分钟" + }, + { + "step": 9, + "description": "将鱼放在案板上,用干毛巾将鱼身上的水擦干(这样可以更好的挂糊)" + }, + { + "step": 10, + "description": "将盆冲洗干净,用干毛巾擦干" + }, + { + "step": 11, + "description": "起锅烧油,加入约 1L 的油,将油温烧至 7 成热,约 200-240 度" + }, + { + "step": 12, + "description": "捏起鱼的尾巴,将鱼头沉入锅底,用勺子往鱼的身上淋热油,待面糊成型后,将鱼慢慢放入锅中,拿锅铲轻轻铲起鱼的头部,然后垫上笊篱。防止底部炸糊。" + }, + { + "step": 13, + "description": "准备一个盛鱼的盘子,放在锅的旁边。" + }, + { + "step": 14, + "description": "用锅铲从鱼身处轻轻铲入,两个工具配合鱼翻个身。再炸两分钟,还是同样的方式(笊篱托着鱼头,锅铲托着鱼身,将鱼盛入盘中)" + }, + { + "step": 15, + "description": "将锅中的油倒入擦干的盆中,放置一边,然后将锅刷干净" + }, + { + "step": 16, + "description": "将 50g 清水、40g 番茄酱、20g 白糖、10g 白醋放入小碗中,搅拌均匀" + }, + { + "step": 17, + "description": "再准备一个小碗加入 10g 淀粉、10g 水,搅拌成水淀粉" + }, + { + "step": 18, + "description": "开大火将锅烧热,然后倒入之前准备的料汁,大火烧开,转小火" + }, + { + "step": 19, + "description": "加入调好的水淀粉,边倒边搅拌,然后 20 秒后关火" + }, + { + "step": 20, + "description": "将熬好的糖醋汁用勺子均匀地浇在鱼身上,可以加点香菜或葱花点缀,糖醋鲤鱼就做好了" + }, + { + "step": 21, + "description": "![成品](./成品.jpg)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-芥末黄油罗氏虾-芥末黄油罗氏虾", + "name": "芥末黄油罗氏虾的做法", + "description": "# 芥末黄油罗氏虾的做法\n\n![芥末黄油罗氏虾](./芥末黄油罗氏虾.jpg)\n这是一道做法简单,味道美味,具有新意的海鲜菜。\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/芥末黄油罗氏虾/芥末黄油罗氏虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/芥末黄油罗氏虾/芥末黄油罗氏虾.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/芥末黄油罗氏虾/芥末黄油罗氏虾.jpg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "罗氏虾", + "quantity": null, + "unit": null, + "text_quantity": "- 罗氏虾", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "芥末", + "quantity": null, + "unit": null, + "text_quantity": "- 芥末", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "料酒、朗姆酒或啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒、朗姆酒或啤酒", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "罗氏虾", + "quantity": null, + "unit": null, + "text_quantity": "- 罗氏虾 1 斤多 广东市场价大概 40~45 一斤", + "notes": "量未指定" + }, + { + "name": "黄油 约", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 约 20g", + "notes": "量未指定" + }, + { + "name": "芥末", + "quantity": null, + "unit": null, + "text_quantity": "- 芥末 15g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 3g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 30g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 30g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "料酒、朗姆酒或啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒、朗姆酒或啤酒 15g 到 30g", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 5 条 切段", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 5 颗 剁成蒜蓉", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将罗氏虾剪掉头尾尖刺、触须和脚,剪刀把虾身开背,去除虾线。" + }, + { + "step": 2, + "description": "提前搅拌好芥末酱汁:酱油、蚝油、芥末、盐、糖,搅拌均匀!" + }, + { + "step": 3, + "description": "洗好香菜,切段备用。" + }, + { + "step": 4, + "description": "罗氏虾沥掉水,锅中加入油,直接放入罗氏虾,中火,外表煎至金黄,捞出。" + }, + { + "step": 5, + "description": "下入蒜蓉,大火,利用煎虾剩下的油继续煎炒蒜蓉,等到锅中白雾冒出,蒜蓉已经煎出香味,下虾和黄油,让虾充分吸收黄油香味" + }, + { + "step": 6, + "description": "下入调好的酱汁,继续大火煮沸,翻炒虾,至酱汁收汁,加入酒(料酒、啤酒可以放 30g,朗姆酒味道浓郁放 15g 即可。)" + }, + { + "step": 7, + "description": "在等酱汁稍微收汁,加入香菜翻炒两下,即可出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-葱油桂鱼-葱油桂鱼", + "name": "葱油桂鱼的做法", + "description": "# 葱油桂鱼的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/葱油桂鱼/葱油桂鱼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/10.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/10.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/葱油桂鱼.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/葱油鲈鱼.jpg" + ], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "桂鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 桂鱼", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "蒸鱼豉油", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸鱼豉油", + "notes": "量未指定" + }, + { + "name": "蒸笼(含蒸锅)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸笼(含蒸锅)", + "notes": "量未指定" + }, + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水", + "notes": "量未指定" + }, + { + "name": "砧板", + "quantity": null, + "unit": null, + "text_quantity": "- 砧板", + "notes": "量未指定" + }, + { + "name": "铁锅", + "quantity": null, + "unit": null, + "text_quantity": "- 铁锅", + "notes": "量未指定" + }, + { + "name": "塑料盘或塑料盆(腌鱼用)", + "quantity": null, + "unit": null, + "text_quantity": "- 塑料盘或塑料盆(腌鱼用)", + "notes": "量未指定" + }, + { + "name": "一次性手套", + "quantity": null, + "unit": null, + "text_quantity": "- 一次性手套", + "notes": "量未指定" + }, + { + "name": "厨房纸", + "quantity": null, + "unit": null, + "text_quantity": "- 厨房纸", + "notes": "量未指定" + }, + { + "name": "蒸鱼盘子(能平放下一条鱼即可)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸鱼盘子(能平放下一条鱼即可)", + "notes": "量未指定" + }, + { + "name": "菜刀", + "quantity": null, + "unit": null, + "text_quantity": "- 菜刀", + "notes": "量未指定" + }, + { + "name": "削皮刀", + "quantity": null, + "unit": null, + "text_quantity": "- 削皮刀", + "notes": "量未指定" + }, + { + "name": "防烫盘夹(或者防烫手套)", + "quantity": null, + "unit": null, + "text_quantity": "- 防烫盘夹(或者防烫手套)", + "notes": "量未指定" + }, + { + "name": "桂鱼 =", + "quantity": null, + "unit": null, + "text_quantity": "- 桂鱼 = 1 斤(500g)", + "notes": "量未指定" + }, + { + "name": "小葱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 = 1 根(长度为 30cm)", + "notes": "量未指定" + }, + { + "name": "小米辣 =", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 = 2 个", + "notes": "量未指定" + }, + { + "name": "姜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 = 50g", + "notes": "量未指定" + }, + { + "name": "料酒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 = 25g", + "notes": "量未指定" + }, + { + "name": "植物油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油 = 15g", + "notes": "量未指定" + }, + { + "name": "盐 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = 8g", + "notes": "量未指定" + }, + { + "name": "蒸鱼豉油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸鱼豉油 = 10g", + "notes": "量未指定" + }, + { + "name": "清水 =", + "quantity": null, + "unit": null, + "text_quantity": "- 清水 = 5L", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "去菜市场买已经处理好的鱼(自己处理的话最好不要内脏),将鱼身表面的所有鳞片刮干净" + }, + { + "step": 2, + "description": "用厨房用纸将鱼肚子里的贴骨血和黑膜擦干净(帖骨血会影响口感,黑膜是鱼腥味的来源)" + }, + { + "step": 3, + "description": "用菜刀在鱼身表面来回刮几次,将鱼身的黏液刮掉,进一步去除腥味,然后用清水将鱼内外冲洗干净" + }, + { + "step": 4, + "description": "将鱼平放在砧板,使用厨房纸将鱼内外的水分擦干,然后鱼头朝左,尾朝右,从鱼鳃边开始,每隔 3cm 纵向划一刀,深度达到鱼的脊椎骨即可,另一面使用同样的处理方式" + }, + { + "step": 5, + "description": "将鱼平放在盆中,确保盘中没有多余水分" + }, + { + "step": 6, + "description": "取一块 50g 姜(鸡蛋大小),用削皮刀把表面的皮去除并洗干净,然后切成厚度为 3mm 的姜片" + }, + { + "step": 7, + "description": "将小米辣洗干净、去蒂,切成厚度为 2mm 的小圆片(或切成 1mm 宽度的丝状)" + }, + { + "step": 8, + "description": "将小葱洗干净,去除根须,切成 3cm 的小段,稍微粗一点的小葱,可以沿着小葱生长的方向沿中间劈开" + }, + { + "step": 9, + "description": "加入 8g 盐,25g 料酒到盆中,带上一次性手套,然后对鱼进行全身按摩 1 分钟,确保鱼身每个部位都均匀涂抹了盐和料酒" + }, + { + "step": 10, + "description": "按摩好鱼后,在鱼身的每一个刀口中塞入一片姜片,鱼肚子中放入 3 片姜片,腌制 10 分钟(建议不要腌制太久,否则鱼的鲜度降低)" + }, + { + "step": 11, + "description": "在鱼腌制期间,在蒸锅中加入 5L 清水,烧开后,在蒸锅上放上蒸笼" + }, + { + "step": 12, + "description": "鱼腌制好后,会析出水分,将多余水分和腌制用料酒、姜片倒掉,用清水冲洗干净鱼身和鱼肚,用厨房纸擦干鱼身和鱼肚" + }, + { + "step": 13, + "description": "将鱼平放在蒸鱼盘中,重新在鱼身、鱼肚刀口处塞入姜片" + }, + { + "step": 14, + "description": "然后将蒸鱼盘放入蒸笼中,盖上盖子,中火蒸 20 分钟" + }, + { + "step": 15, + "description": "期间水蒸气会附着整个鱼和盘子上,凝结后形成鱼汤,出锅后千万不要倒掉这个汤,这个汤汁是鲜味精华" + }, + { + "step": 16, + "description": "用防烫夹将蒸鱼盘夹出,在鱼身和鱼周围淋上 10g 蒸鱼豉油" + }, + { + "step": 17, + "description": "然后在鱼身和周围均匀撒上小葱段和小米辣" + }, + { + "step": 18, + "description": "在铁锅中倒入 15g 植物油,用中小火慢熬 5 分钟,不要用大火,否则油会挥发很快" + }, + { + "step": 19, + "description": "将出锅后的热油均匀地慢慢地淋在鱼身上,鲜掉眉毛的葱油桂鱼就出炉啦!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-葱烧海参-葱烧海参", + "name": "葱烧海参的做法", + "description": "# 葱烧海参的做法\n\n![示例菜成品](./葱烧海参.jpeg)\n\n这道菜的做法并不难,就是海参泡发是需要时间的。疫情隔离在家,干海参是过年前存的年货,正好拿出来尝试一下。\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/葱烧海参/葱烧海参.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/海参.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/海参.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/葱烧海参.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/葱白.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/酱汁.jpeg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "泡发好的海参![海参](./海参.jpeg)", + "quantity": null, + "unit": null, + "text_quantity": "- 泡发好的海参![海参](./海参.jpeg)", + "notes": "量未指定" + }, + { + "name": "大葱葱白", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱葱白", + "notes": "量未指定" + }, + { + "name": "泡发好的海参(北极参)", + "quantity": null, + "unit": null, + "text_quantity": "- 泡发好的海参(北极参) 4 个", + "notes": "量未指定" + }, + { + "name": "大葱葱白", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱葱白 1 根大葱的葱白即可", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 20-25ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 20g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 2g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "葱白切成 1cm 的段,备用。" + }, + { + "step": 2, + "description": "海参切成 1cm 的段,备用。" + }, + { + "step": 3, + "description": "准备一个空碗,倒入 20g 蚝油, 10g 生抽, 2g 白糖,搅拌均匀。![料汁](./酱汁.jpeg)" + }, + { + "step": 4, + "description": "另一个空碗倒入淀粉,水,制备水淀粉,勾芡用。" + }, + { + "step": 5, + "description": "热锅,锅内放入 20ml - 25ml 食用油。等待 10 秒让油温升高。" + }, + { + "step": 6, + "description": "放入葱白,调*小火*,注意不要让葱白变焦。大概煎 3-5 分钟即可。![葱白](./葱白.jpeg)" + }, + { + "step": 7, + "description": "用筷子夹出葱白,放入盘中备用。" + }, + { + "step": 8, + "description": "倒入调好的料汁,炒香,**等待 1 - 2 分钟** 。" + }, + { + "step": 9, + "description": "放入切好的海参,翻炒 1 分钟" + }, + { + "step": 10, + "description": "加入 100 ml 的水, 中小火, **等待 5 分钟**" + }, + { + "step": 11, + "description": "等待锅中汤汁快干的时候,加入水淀粉,加入前面取出的葱白" + }, + { + "step": 12, + "description": "在外观*呈粘稠状态*后关火,盛盘 ![成品](./葱烧海参.jpeg)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-蒜蓉虾-蒜蓉虾", + "name": "蒜蓉虾的做法", + "description": "# 蒜蓉虾的做法\n\n蒜蓉虾是广东省地方传统名菜,色香味俱全。\n\n预估烹饪难度:★★", + "source_path": "dishes/aquatic/蒜蓉虾/蒜蓉虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜蓉虾/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜蓉虾/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜蓉虾/2.jpeg" + ], + "category": "水产", + "difficulty": 2, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "海虾", + "quantity": null, + "unit": null, + "text_quantity": "- 海虾", + "notes": "量未指定" + }, + { + "name": "蒜蓉酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜蓉酱", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "海虾", + "quantity": null, + "unit": null, + "text_quantity": "- 海虾 8 只", + "notes": "量未指定" + }, + { + "name": "蒜蓉酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜蓉酱 50 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 20 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "用刀从从虾头中间切开,切到距离虾尾 1 cm" + }, + { + "step": 2, + "description": "将蒜蓉酱铺在虾身中间,放在盘子中" + }, + { + "step": 3, + "description": "锅中倒入热水,将盘子放入锅中,大火蒸 3 分钟" + }, + { + "step": 4, + "description": "烧热油,倒入虾盘中,倒入生抽" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-蒜香黄油虾-蒜香黄油虾", + "name": "蒜香黄油虾的做法", + "description": "# 蒜香黄油虾的做法\n\n蒜香黄油虾是一道经典的西式海鲜料理,以鲜虾为主料,配以蒜末和黄油烹制而成。口感鲜嫩,蒜香浓郁。制作简单,适合家庭日常烹饪。\n\n![蒜香黄油虾](./1.jpg)\n\n预估烹饪难度:★★", + "source_path": "dishes/aquatic/蒜香黄油虾/蒜香黄油虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜香黄油虾/1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜香黄油虾/1.jpg" + ], + "category": "水产", + "difficulty": 2, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "大虾(推荐黑虎虾或基围虾)", + "quantity": null, + "unit": null, + "text_quantity": "- 大虾(推荐黑虎虾或基围虾)", + "notes": "量未指定" + }, + { + "name": "无盐黄油(推荐安佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 无盐黄油(推荐安佳)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "白葡萄酒(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 白葡萄酒(可选)", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "平底煎锅", + "quantity": null, + "unit": null, + "text_quantity": "- 平底煎锅", + "notes": "量未指定" + }, + { + "name": "厨房用夹", + "quantity": null, + "unit": null, + "text_quantity": "- 厨房用夹", + "notes": "量未指定" + }, + { + "name": "大虾", + "quantity": null, + "unit": null, + "text_quantity": "- 大虾 8-10 只(约 200g)", + "notes": "量未指定" + }, + { + "name": "无盐黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 无盐黄油 30g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 4 瓣(约 20g)", + "notes": "量未指定" + }, + { + "name": "白葡萄酒", + "quantity": null, + "unit": null, + "text_quantity": "- 白葡萄酒 15ml(可选)", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 1/4 个", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油 10ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "大虾去头去壳留尾,用牙签挑去虾线,洗净后用厨房纸吸干水分" + }, + { + "step": 2, + "description": "大蒜切成蒜末,备用" + }, + { + "step": 3, + "description": "中火加热平底锅,放入 10ml 橄榄油" + }, + { + "step": 4, + "description": "油热后放入大虾,每面煎 1-1.5 分钟至变色,取出备用" + }, + { + "step": 5, + "description": "同一锅中加入黄油,融化后放入蒜末,小火炒香(约 30 秒)" + }, + { + "step": 6, + "description": "如使用白葡萄酒,此时加入并煮至酒精挥发(约 1 分钟)" + }, + { + "step": 7, + "description": "将虾放回锅中,与蒜香黄油酱汁翻炒均匀(约 1 分钟)" + }, + { + "step": 8, + "description": "挤入柠檬汁,翻炒均匀后立即关火" + }, + { + "step": 9, + "description": "装盘,淋上锅中剩余酱汁" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-蛏抱蛋-蛏抱蛋", + "name": "蛏抱蛋的做法", + "description": "# 蛏抱蛋的做法\n\n蛏抱蛋,是流行于福建省福州地区的传统家常菜\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/蛏抱蛋/蛏抱蛋.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/3.jpeg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "蛏子", + "quantity": null, + "unit": null, + "text_quantity": "- 蛏子", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "蛏子", + "quantity": null, + "unit": null, + "text_quantity": "- 蛏子 200 g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 100 ml", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 0.25 个", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 20 g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 5 ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 5 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "烧开水,将蛏子放入水中,水煮 2 分钟后,捞上来去壳,放入大碗" + }, + { + "step": 2, + "description": "往大碗中加入洋葱、生抽、料酒、鸡精、生粉后,充分搅拌" + }, + { + "step": 3, + "description": "往大碗中打入 2 个 鸡蛋,继续搅拌" + }, + { + "step": 4, + "description": "起锅烧油,倒入碗中蛏子,煎炸至单面金黄后,翻面继续煎炸" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-香煎翘嘴鱼-香煎翘嘴鱼", + "name": "香煎翘嘴鱼的做法", + "description": "# 香煎翘嘴鱼的做法\n\n![香煎翘嘴鱼](./香煎翘嘴鱼.jpeg)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/aquatic/香煎翘嘴鱼/香煎翘嘴鱼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/香煎翘嘴鱼/香煎翘嘴鱼.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/香煎翘嘴鱼/香煎翘嘴鱼.jpeg" + ], + "category": "水产", + "difficulty": 4, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "翘嘴鱼(肉食性鱼类,肉细腻,口感好)", + "quantity": null, + "unit": null, + "text_quantity": "- 翘嘴鱼(肉食性鱼类,肉细腻,口感好)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "翘嘴鱼:2 斤最佳", + "quantity": null, + "unit": null, + "text_quantity": "- 翘嘴鱼:2 斤最佳", + "notes": "量未指定" + }, + { + "name": "姜沫:20g", + "quantity": null, + "unit": null, + "text_quantity": "- 姜沫:20g", + "notes": "量未指定" + }, + { + "name": "葱:半根(50 克)", + "quantity": null, + "unit": null, + "text_quantity": "- 葱:半根(50 克)", + "notes": "量未指定" + }, + { + "name": "蒜:4 个", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜:4 个", + "notes": "量未指定" + }, + { + "name": "香菜:个人口味", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜:个人口味", + "notes": "量未指定" + }, + { + "name": "老抽:2ml(不太喜欢重口的可以不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽:2ml(不太喜欢重口的可以不放)", + "notes": "量未指定" + }, + { + "name": "白糖:10g", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖:10g", + "notes": "量未指定" + }, + { + "name": "干辣椒:4-6 个(根据个人口味选择)", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒:4-6 个(根据个人口味选择)", + "notes": "量未指定" + }, + { + "name": "料酒:100ml", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒:100ml", + "notes": "量未指定" + }, + { + "name": "生抽:4ml", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽:4ml", + "notes": "量未指定" + }, + { + "name": "盐:约", + "quantity": null, + "unit": null, + "text_quantity": "- 盐:约 50g 用于腌制鱼", + "notes": "量未指定" + }, + { + "name": "食用油:100ml", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油:100ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鱼开背杀好(让卖鱼的杀好,千万不要剖腹杀鱼,切记是开背),清洗干净" + }, + { + "step": 2, + "description": "鱼表面用盐涂抹均匀,倒入料酒约 80ml,姜末 20g,放入冰箱保鲜层进行腌制 1-2 天" + }, + { + "step": 3, + "description": "取出腌制好的鱼,用绳挂起晾晒至半干(约 1-2 天,具体时间需结合气温与阳光)" + }, + { + "step": 4, + "description": "食用前请将鱼用清水清洗,沥干水分(防止水遇油飞溅)" + }, + { + "step": 5, + "description": "开大火将锅烧热,迅速改小火,锅中放油,尽量保持整个锅表面有油,将鱼沿锅边划入锅内(先煎鱼背面)" + }, + { + "step": 6, + "description": "鱼入锅后(和翻面后),不要着急移动鱼的位置(此时容易破皮),煎约 30 秒后,尝试晃动锅" + }, + { + "step": 7, + "description": "背面煎约 1 分钟后,翻面煎约 1-2 分钟,煎至两面金黄" + }, + { + "step": 8, + "description": "等两面都煎好时,把鱼推向锅边一点,留点空间放入豆瓣酱炒香味,放入姜蒜," + }, + { + "step": 9, + "description": "炒出佐料香味后,加入料酒,生抽,老抽,倒入热水,水量和鱼平齐或者少点" + }, + { + "step": 10, + "description": "此时改中大火,煮 5-10 分钟,后放入青椒断,白糖,鸡精,十三香,陈醋" + }, + { + "step": 11, + "description": "改小火 2-5 分钟,放入葱,香菜,即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-鲤鱼炖白菜-鲤鱼炖白菜", + "name": "鲤鱼炖白菜的做法", + "description": "# 鲤鱼炖白菜的做法\n\n![鲤鱼炖白菜](./鲤鱼炖白菜.jpeg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/鲤鱼炖白菜/鲤鱼炖白菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鲤鱼炖白菜/鲤鱼炖白菜.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鲤鱼炖白菜/鲤鱼炖白菜.jpeg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "鲤鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 鲤鱼", + "notes": "量未指定" + }, + { + "name": "白菜心/娃娃菜", + "quantity": null, + "unit": null, + "text_quantity": "- 白菜心/娃娃菜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣酱", + "notes": "量未指定" + }, + { + "name": "干辣椒(不吃辣可以不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒(不吃辣可以不放)", + "notes": "量未指定" + }, + { + "name": "食用油:10ml", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油:10ml", + "notes": "量未指定" + }, + { + "name": "姜:3 片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜:3 片", + "notes": "量未指定" + }, + { + "name": "蒜:3 瓣(切成块)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜:3 瓣(切成块)", + "notes": "量未指定" + }, + { + "name": "鲤鱼:1.2 斤(清理过的)", + "quantity": null, + "unit": null, + "text_quantity": "- 鲤鱼:1.2 斤(清理过的)", + "notes": "量未指定" + }, + { + "name": "娃娃菜:13 片(可以多放一些,一顿就缩小了)", + "quantity": null, + "unit": null, + "text_quantity": "- 娃娃菜:13 片(可以多放一些,一顿就缩小了)", + "notes": "量未指定" + }, + { + "name": "盐:5-8 克", + "quantity": null, + "unit": null, + "text_quantity": "- 盐:5-8 克", + "notes": "量未指定" + }, + { + "name": "老抽:3ml", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽:3ml", + "notes": "量未指定" + }, + { + "name": "生抽:6ml", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽:6ml", + "notes": "量未指定" + }, + { + "name": "桂皮:1 块", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮:1 块", + "notes": "量未指定" + }, + { + "name": "八角:3 个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角:3 个", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣酱:20 克", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣酱:20 克", + "notes": "量未指定" + }, + { + "name": "干辣椒:4-6 个(根据个人口味选择)", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒:4-6 个(根据个人口味选择)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鲤鱼清洗干净,改刀(在鱼身上多划几个伤口,方便入味)" + }, + { + "step": 2, + "description": "娃娃菜清洗干净放入盘中备用" + }, + { + "step": 3, + "description": "锅中加油,等油热放入“少盐” “姜” “蒜” “郫县豆瓣酱” “桂皮” “八角” 炒出香味" + }, + { + "step": 4, + "description": "把鱼放锅里煎(3 分钟)每(30 秒)需要翻面" + }, + { + "step": 5, + "description": "加入“水”(水量尽量和鱼平齐,可以少一点点) 放入 “生抽” “老抽” “娃娃菜”" + }, + { + "step": 6, + "description": "大火炖 15-20 分钟,汤汁快干时添加 “盐” 即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-鳊鱼炖豆腐-鳊鱼炖豆腐", + "name": "鳊鱼炖豆腐的做法", + "description": "# 鳊鱼炖豆腐的做法\n\n![鳊鱼炖豆腐](./鳊鱼炖豆腐.jpg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/鳊鱼炖豆腐/鳊鱼炖豆腐.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鳊鱼炖豆腐/鳊鱼炖豆腐.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鳊鱼炖豆腐/鳊鱼炖豆腐.jpg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "鳊鱼(鱼可以让摊主帮忙处理好)", + "quantity": null, + "unit": null, + "text_quantity": "- 鳊鱼(鱼可以让摊主帮忙处理好)", + "notes": "量未指定" + }, + { + "name": "老豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 老豆腐", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "桂皮(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮(可选)", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "干辣椒(不吃辣可以不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒(不吃辣可以不放)", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "八角(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 八角(可选)", + "notes": "量未指定" + }, + { + "name": "香叶(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶(可选)", + "notes": "量未指定" + }, + { + "name": "热水", + "quantity": null, + "unit": null, + "text_quantity": "- 热水", + "notes": "量未指定" + }, + { + "name": "鳊鱼:550 克", + "quantity": null, + "unit": null, + "text_quantity": "- 鳊鱼:550 克", + "notes": "量未指定" + }, + { + "name": "老豆腐:400 克", + "quantity": null, + "unit": null, + "text_quantity": "- 老豆腐:400 克", + "notes": "量未指定" + }, + { + "name": "姜:5 片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜:5 片", + "notes": "量未指定" + }, + { + "name": "葱:半根(50 克)", + "quantity": null, + "unit": null, + "text_quantity": "- 葱:半根(50 克)", + "notes": "量未指定" + }, + { + "name": "蒜:4 个", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜:4 个", + "notes": "量未指定" + }, + { + "name": "老抽:2ml(不太喜欢重口的可以不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽:2ml(不太喜欢重口的可以不放)", + "notes": "量未指定" + }, + { + "name": "桂皮:1 块", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮:1 块", + "notes": "量未指定" + }, + { + "name": "冰糖:5 块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖:5 块", + "notes": "量未指定" + }, + { + "name": "干辣椒:4-6 个(根据个人口味选择)", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒:4-6 个(根据个人口味选择)", + "notes": "量未指定" + }, + { + "name": "料酒:5ml", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒:5ml", + "notes": "量未指定" + }, + { + "name": "生抽:4ml", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽:4ml", + "notes": "量未指定" + }, + { + "name": "盐:5-8 克(根据个人口味选择)", + "quantity": null, + "unit": null, + "text_quantity": "- 盐:5-8 克(根据个人口味选择)", + "notes": "量未指定" + }, + { + "name": "八角:1 个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角:1 个", + "notes": "量未指定" + }, + { + "name": "香叶:1-3 片", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶:1-3 片", + "notes": "量未指定" + }, + { + "name": "食用油:10ml", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油:10ml", + "notes": "量未指定" + }, + { + "name": "热水:400 克", + "quantity": null, + "unit": null, + "text_quantity": "- 热水:400 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鳊鱼改刀,放上姜片和料酒腌制 5-10 分钟" + }, + { + "step": 2, + "description": "老豆腐切块后放入水中备用" + }, + { + "step": 3, + "description": "锅中加油,可以放点盐在锅里,防止煎鱼的时候粘锅,把腌制的鱼用厨房纸擦干水分,把鱼放到锅中,两面都煎一下" + }, + { + "step": 4, + "description": "等两面都煎好时,把鱼推向锅边一点,留点空间放入葱姜蒜,干辣椒,香叶,八角炒出味道" + }, + { + "step": 5, + "description": "炒出佐料香味后,加入料酒,生抽,老抽,冰糖,桂皮,倒入热水,水量和鱼平齐或者少点" + }, + { + "step": 6, + "description": "大火烧开后,放入老豆腐,豆腐贴在锅边,加入食盐,转小火" + }, + { + "step": 7, + "description": "小火烧 10-15 分钟,然后大火收点汁,即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-aquatic-黄油煎虾-黄油煎虾", + "name": "黄油煎虾的做法", + "description": "# 黄油煎虾的做法\n\n![示例菜成品](./黄油煎虾.jpg)\n\n黄油煎虾是一道制作相对简单、风味极佳的菜式,主要耗时在于处理活虾,总耗时在一个小时内,适合初学者进行烹饪。\n\n预估烹饪难度:★★★", + "source_path": "dishes/aquatic/黄油煎虾/黄油煎虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/黄油煎虾/黄油煎虾.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/黄油煎虾/黄油煎虾.jpg" + ], + "category": "水产", + "difficulty": 3, + "tags": [ + "水产" + ], + "servings": 1, + "ingredients": [ + { + "name": "鲜虾(强推肉质紧实的九节虾,普通明虾也可以)", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜虾(强推肉质紧实的九节虾,普通明虾也可以)", + "notes": "量未指定" + }, + { + "name": "黄油(推荐安佳,一次用一小盒", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油(推荐安佳,一次用一小盒 7g)", + "notes": "量未指定" + }, + { + "name": "黑胡椒粒(瓶磨的那种)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粒(瓶磨的那种)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "米酒", + "quantity": null, + "unit": null, + "text_quantity": "- 米酒", + "notes": "量未指定" + }, + { + "name": "鲜虾", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜虾 300g", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 7g", + "notes": "量未指定" + }, + { + "name": "黑胡椒粒 大概", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粒 大概 15ml", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 45ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "米酒", + "quantity": null, + "unit": null, + "text_quantity": "- 米酒 5ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 10ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2.5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鲜虾摘除头部,顺带扯出虾线(这步处理不好可在下一步开背时取出虾线),使用剪刀剪开或菜刀片开虾背,沥干水分备用" + }, + { + "step": 2, + "description": "调制酱汁:小碗放入上述量的全部生抽、米酒、白糖、盐搅匀备用" + }, + { + "step": 3, + "description": "中大火热锅,热锅内放入食用油,等待 10 秒让油温升高" + }, + { + "step": 4, + "description": "虾全部放入锅中,开始瓶磨黑胡椒,均匀地撒在虾上翻炒" + }, + { + "step": 5, + "description": "虾变色后加入黄油,黄油完全融化后倒入调制酱汁,继续翻炒" + }, + { + "step": 6, + "description": "大火翻炒 15 秒收汁即可装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-吐司果酱", + "name": "吐司果酱的做法", + "description": "# 吐司果酱的做法\n\n饱腹感的懒人快速营养早餐,2 分钟 搞定\n\n预估烹饪难度:★", + "source_path": "dishes/breakfast/吐司果酱.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 1, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "新鲜吐司", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜吐司", + "notes": "量未指定" + }, + { + "name": "果酱", + "quantity": null, + "unit": null, + "text_quantity": "- 果酱", + "notes": "量未指定" + }, + { + "name": "面包机", + "quantity": null, + "unit": null, + "text_quantity": "- 面包机", + "notes": "量未指定" + }, + { + "name": "吐司两片", + "quantity": null, + "unit": null, + "text_quantity": "- 吐司两片", + "notes": "量未指定" + }, + { + "name": "果酱足够涂满一面吐司的量", + "quantity": null, + "unit": null, + "text_quantity": "- 果酱足够涂满一面吐司的量", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将吐司放入面包机" + }, + { + "step": 2, + "description": "设置好档位,时间到了会自动弹出" + }, + { + "step": 3, + "description": "两分钟后吐司加热完成弹出" + }, + { + "step": 4, + "description": "先取出一片吐司,涂满果酱再盖上另一片吐司即可" + }, + { + "step": 5, + "description": "用餐巾纸包一下可以边走边吃也可以吃完再出门" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-太阳蛋", + "name": "太阳蛋的做法", + "description": "# 太阳蛋的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/太阳蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "分可控火候微波炉或不可控火候微波炉(定义和分辨方式请见附加内容)", + "quantity": null, + "unit": null, + "text_quantity": "- 分可控火候微波炉或不可控火候微波炉(定义和分辨方式请见附加内容)", + "notes": "量未指定" + }, + { + "name": "筷子或牙签", + "quantity": null, + "unit": null, + "text_quantity": "- 筷子或牙签", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "准备一个小碗,倒入在上一步计算好的油,撒盐,搅拌均匀。倾斜碗使油沾在碗表面。" + }, + { + "step": 2, + "description": "取出一个鸡蛋,打入小碗。" + }, + { + "step": 3, + "description": "蛋黄表面戳孔。牙签戳 5 个或筷子戳 1 个。" + }, + { + "step": 4, + "description": "放入微波炉,中火 3 分钟。" + }, + { + "step": 5, + "description": "准备一个小碗,倒入在上一步计算好的油,撒盐,搅拌均匀。倾斜碗使油沾在碗表面。" + }, + { + "step": 6, + "description": "取出一个鸡蛋,打入小碗。" + }, + { + "step": 7, + "description": "蛋黄表面戳孔。牙签戳 5 个或筷子戳 1 个。" + }, + { + "step": 8, + "description": "放入微波炉,1 分钟。" + }, + { + "step": 9, + "description": "while(太阳蛋 否 大面积成固体状) 用微波炉打(30s);" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-完美水煮蛋", + "name": "完美水煮蛋的做法", + "description": "# 完美水煮蛋的做法\n\n![完美水煮蛋](https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1yBdnK.img?w=768&h=512&m=6)\n\n科学家研发的循环水煮法,可同时达到蛋黄绵密、蛋白均匀凝固且保留最多营养素的效果。需精准控制温度与时间,难度较高。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/breakfast/完美水煮蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 5, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "新鲜鸡蛋(推荐 AA 级)", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜鸡蛋(推荐 AA 级)", + "notes": "量未指定" + }, + { + "name": "100°C 沸水锅(直径≥", + "quantity": null, + "unit": null, + "text_quantity": "- 100°C 沸水锅(直径≥ 15cm)", + "notes": "量未指定" + }, + { + "name": "30°C 温水锅(直径≥", + "quantity": null, + "unit": null, + "text_quantity": "- 30°C 温水锅(直径≥ 15cm)", + "notes": "量未指定" + }, + { + "name": "定时器", + "quantity": null, + "unit": null, + "text_quantity": "- 定时器", + "notes": "量未指定" + }, + { + "name": "漏勺", + "quantity": null, + "unit": null, + "text_quantity": "- 漏勺", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个(约 60g )", + "notes": "量未指定" + }, + { + "name": "100°C 沸水", + "quantity": null, + "unit": null, + "text_quantity": "- 100°C 沸水 1500ml", + "notes": "量未指定" + }, + { + "name": "30°C 温水", + "quantity": null, + "unit": null, + "text_quantity": "- 30°C 温水 1500ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "准备两锅水: A 锅维持 100°C 沸水, B 锅维持 30°C 温水" + }, + { + "step": 2, + "description": "用漏勺将鸡蛋放入 A 锅,启动定时器" + }, + { + "step": 3, + "description": "精准**每 2 分钟**将鸡蛋转移至另一锅水" + }, + { + "step": 4, + "description": "重复转移操作共 16 次(总时长 32 分钟)" + }, + { + "step": 5, + "description": "最后一次转移后,在 B 锅静置 30 秒" + }, + { + "step": 6, + "description": "立即放入冰水( 0 摄氏度)终止加热(维持 30 秒)" + }, + { + "step": 7, + "description": "剥壳时从钝端气室处开始,沿纵轴剥离蛋膜" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-微波炉荷包蛋", + "name": "微波炉荷包蛋的做法", + "description": "# 微波炉荷包蛋的做法\n\n微波炉荷包蛋是一道简单易做且富含蛋白质的菜。只需要微波炉 120 秒内就可以完成,适合通勤社畜早餐。\n\n预估烹饪难度:★", + "source_path": "dishes/breakfast/微波炉荷包蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 1, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "芝麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 35ml", + "notes": "量未指定" + }, + { + "name": "芝麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油 3ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 0.8g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将鸡蛋打入小碗中,用筷子在所有鸡蛋黄上扎 2 个洞,避免加热弄脏微波炉" + }, + { + "step": 2, + "description": "然后向碗内倒入常温饮用水" + }, + { + "step": 3, + "description": "再向碗内倒入食用盐" + }, + { + "step": 4, + "description": "最后加入芝麻油" + }, + { + "step": 5, + "description": "将放好材料的碗放入微波炉中,高火加热 80 秒" + }, + { + "step": 6, + "description": "到达设定时间后,使用抹布垫着手取出成品" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-微波炉蛋糕", + "name": "微波炉蛋糕的做法", + "description": "# 微波炉蛋糕的做法\n\n微波炉\"叮\"蛋糕,大约需要 2 分钟 就能搞定!初学者所需时间预计延长至 20 分钟。\n\n预估烹饪难度:★", + "source_path": "dishes/breakfast/微波炉蛋糕.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 1, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "微波炉", + "quantity": null, + "unit": null, + "text_quantity": "- 微波炉", + "notes": "量未指定" + }, + { + "name": "能放进微波炉的容器", + "quantity": null, + "unit": null, + "text_quantity": "- 能放进微波炉的容器", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "泡打粉(不加吃着像饼)", + "quantity": null, + "unit": null, + "text_quantity": "- 泡打粉(不加吃着像饼)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "鸡蛋🥚", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋🥚 1 个", + "notes": "量未指定" + }, + { + "name": "面粉🍚", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉🍚 15g", + "notes": "量未指定" + }, + { + "name": "泡打粉🍚", + "quantity": null, + "unit": null, + "text_quantity": "- 泡打粉🍚 2.5g", + "notes": "量未指定" + }, + { + "name": "白(红)糖🍬", + "quantity": null, + "unit": null, + "text_quantity": "- 白(红)糖🍬 10g", + "notes": "量未指定" + }, + { + "name": "盐🧂", + "quantity": null, + "unit": null, + "text_quantity": "- 盐🧂 1g", + "notes": "量未指定" + }, + { + "name": "咖啡粉☕", + "quantity": null, + "unit": null, + "text_quantity": "- 咖啡粉☕", + "notes": "量未指定" + }, + { + "name": "巧克力🍫", + "quantity": null, + "unit": null, + "text_quantity": "- 巧克力🍫", + "notes": "量未指定" + }, + { + "name": "麦片🍿", + "quantity": null, + "unit": null, + "text_quantity": "- 麦片🍿", + "notes": "量未指定" + }, + { + "name": "牛奶🥛", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶🥛", + "notes": "量未指定" + }, + { + "name": "坚果🥜", + "quantity": null, + "unit": null, + "text_quantity": "- 坚果🥜", + "notes": "量未指定" + }, + { + "name": "饼干屑🍪", + "quantity": null, + "unit": null, + "text_quantity": "- 饼干屑🍪", + "notes": "量未指定" + }, + { + "name": "香蕉🍌", + "quantity": null, + "unit": null, + "text_quantity": "- 香蕉🍌", + "notes": "量未指定" + }, + { + "name": "非黑暗料理🍆", + "quantity": null, + "unit": null, + "text_quantity": "- 非黑暗料理🍆", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "加入以下食材,注意不要超过容器的 3/4" + }, + { + "step": 2, + "description": "夸赞一下自己🥰" + }, + { + "step": 3, + "description": "微波炉(高火)加热 **1分钟** (至蓬松蛋糕形态)" + }, + { + "step": 4, + "description": "取出杯子(烫手啊啊啊啊↑)并拍朋友圈就可以吃了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-手抓饼", + "name": "手抓饼的做法", + "description": "# 手抓饼的做法\n\n预估烹饪难度:★★\n\n---", + "source_path": "dishes/breakfast/手抓饼.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "普通面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 普通面粉", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水", + "notes": "量未指定" + }, + { + "name": "冷水", + "quantity": null, + "unit": null, + "text_quantity": "- 冷水", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜", + "notes": "量未指定" + }, + { + "name": "火腿", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿", + "notes": "量未指定" + }, + { + "name": "芝士片", + "quantity": null, + "unit": null, + "text_quantity": "- 芝士片", + "notes": "量未指定" + }, + { + "name": "--", + "quantity": null, + "unit": null, + "text_quantity": "- --", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 200 克", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 100 毫升", + "notes": "量未指定" + }, + { + "name": "冷水", + "quantity": null, + "unit": null, + "text_quantity": "- 冷水 50 毫升", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15 毫升", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3 克", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜 30 克", + "notes": "量未指定" + }, + { + "name": "火腿", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿 30 克", + "notes": "量未指定" + }, + { + "name": "芝士片", + "quantity": null, + "unit": null, + "text_quantity": "- 芝士片 1 片", + "notes": "量未指定" + }, + { + "name": "--", + "quantity": null, + "unit": null, + "text_quantity": "- --", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "--" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-桂圆红枣粥", + "name": "桂圆红枣粥的做法", + "description": "# 桂圆红枣粥的做法\n\n桂圆红枣粥,甜口。补血安神,健脑益智,补养心脾。制作时间需要 70 分钟。\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/桂圆红枣粥.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "糯米(或大米)", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米(或大米)", + "notes": "量未指定" + }, + { + "name": "红枣", + "quantity": null, + "unit": null, + "text_quantity": "- 红枣", + "notes": "量未指定" + }, + { + "name": "桂圆", + "quantity": null, + "unit": null, + "text_quantity": "- 桂圆", + "notes": "量未指定" + }, + { + "name": "糯米", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米 100g", + "notes": "量未指定" + }, + { + "name": "红枣", + "quantity": null, + "unit": null, + "text_quantity": "- 红枣 15 颗", + "notes": "量未指定" + }, + { + "name": "桂圆", + "quantity": null, + "unit": null, + "text_quantity": "- 桂圆 15 颗", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将桂圆肉扒出,用清水洗两次,放入碗中浸泡 10 分钟" + }, + { + "step": 2, + "description": "红枣用清水洗两次,放入碗中浸泡 10 分钟" + }, + { + "step": 3, + "description": "糯米放入电饭锅中,清水淘米两次后,加入 2000ml 水" + }, + { + "step": 4, + "description": "将桂圆和红枣加入电饭锅" + }, + { + "step": 5, + "description": "打开电饭锅煮饭模式,1 小时后粥成" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-水煮玉米", + "name": "水煮玉米的做法", + "description": "# 水煮玉米的做法\n\n大约 15 分钟可以完成制作。\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/水煮玉米.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "新鲜玉米", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜玉米", + "notes": "量未指定" + }, + { + "name": "放得下玉米的锅", + "quantity": null, + "unit": null, + "text_quantity": "- 放得下玉米的锅", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 糖(可选)", + "notes": "量未指定" + }, + { + "name": "一个带皮玉米", + "quantity": null, + "unit": null, + "text_quantity": "- 一个带皮玉米", + "notes": "量未指定" + }, + { + "name": "淹过玉米约半节指头的水", + "quantity": null, + "unit": null, + "text_quantity": "- 淹过玉米约半节指头的水", + "notes": "量未指定" + }, + { + "name": "煮玉米的时候,开始和淡盐水,差不多", + "quantity": null, + "unit": null, + "text_quantity": "- 煮玉米的时候,开始和淡盐水,差不多 2 克盐加 50ml 的水", + "notes": "量未指定" + }, + { + "name": "根据口味选择加或者不加糖(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 根据口味选择加或者不加糖(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将新鲜玉米剥去外皮,剩部分玉米皮入锅" + }, + { + "step": 2, + "description": "加入淹过玉米约半节指头的水,加盐和糖" + }, + { + "step": 3, + "description": "水煮开之后转至小火,加盖继续煮 15-20 分钟,玉米煮久点没事。" + }, + { + "step": 4, + "description": "煮熟后沥干水分,冷却后食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-溏心蛋", + "name": "溏心蛋的做法", + "description": "# 溏心蛋的做法\n\n喜欢健身的小伙伴可以在每颗鸡蛋中获得 6 克蛋白质。大约 15 分钟可以完成制作。\n\n预估烹饪难度:★★★", + "source_path": "dishes/breakfast/溏心蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 3, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "电锅", + "quantity": null, + "unit": null, + "text_quantity": "- 电锅", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "秒表(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 秒表(可选)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 颗或更多(只要您的电锅装得下,不管有几颗鸡蛋都可以)", + "notes": "量未指定" + }, + { + "name": "淹过鸡蛋约", + "quantity": null, + "unit": null, + "text_quantity": "- 淹过鸡蛋约 2 公分的冷水", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将鸡蛋放入电锅中。鸡蛋不可互相堆叠,应皆在底部,并留有空间可以晃动" + }, + { + "step": 2, + "description": "倒入淹过鸡蛋约 2 公分的冷水" + }, + { + "step": 3, + "description": "开盖,使用最大功率加热至水滚起(大约 85 - 95 度,稍微滚动,不需完全沸腾)" + }, + { + "step": 4, + "description": "关火,盖上盖子,让鸡蛋静置。" + }, + { + "step": 5, + "description": "沥干水分,用冷水冲洗鸡蛋约 1 分钟,即可去壳食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-煎饺", + "name": "煎饺的做法", + "description": "# 煎饺的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/煎饺.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "饺子(速冻水饺)", + "quantity": null, + "unit": null, + "text_quantity": "- 饺子(速冻水饺)", + "notes": "量未指定" + }, + { + "name": "饺子一包 (根据个人食量选择, 约", + "quantity": null, + "unit": null, + "text_quantity": "- 饺子一包 (根据个人食量选择, 约 10 - 15 个)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "取出平底锅(不沾平底锅最佳)" + }, + { + "step": 2, + "description": "加入 10ml - 15 ml 食用油" + }, + { + "step": 3, + "description": "开火,放入饺子(尽量平均铺开,不宜堆叠)" + }, + { + "step": 4, + "description": "立刻加入清水,水线没过饺子平均高度的 1/2" + }, + { + "step": 5, + "description": "盖上锅盖(此时炉灶应该处于大火)" + }, + { + "step": 6, + "description": "等待 8 - 10 分钟" + }, + { + "step": 7, + "description": "当锅中水分仅剩 2mm 时, 转中火开始煎制" + }, + { + "step": 8, + "description": "当水分全部蒸发后,摇晃平底锅使饺子受热均匀" + }, + { + "step": 9, + "description": "放入黑芝麻和葱花再焖 10s" + }, + { + "step": 10, + "description": "1 - 2 分钟夹出一个饺子观察底部,若出现金黄色脆皮立即取出" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-燕麦鸡蛋饼", + "name": "燕麦鸡蛋饼的做法", + "description": "# 燕麦鸡蛋饼的做法\n\n燕麦鸡蛋饼是极具营养、便于制作、适宜快速制作的早餐。尤其适宜热爱健身的上班族。\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/燕麦鸡蛋饼.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "燕麦", + "quantity": null, + "unit": null, + "text_quantity": "- 燕麦", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 50-100g,能够将燕麦搅拌粘稠即可", + "notes": "量未指定" + }, + { + "name": "可根据口味选择增加", + "quantity": null, + "unit": null, + "text_quantity": "- 可根据口味选择增加 50g 蔬菜,如菠菜。", + "notes": "量未指定" + }, + { + "name": "鸡蛋两个,亦可选择两个蛋清,一个蛋黄。", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋两个,亦可选择两个蛋清,一个蛋黄。", + "notes": "量未指定" + }, + { + "name": "纯干燕麦片", + "quantity": null, + "unit": null, + "text_quantity": "- 纯干燕麦片 50g (大约等同一个鸡蛋的量)", + "notes": "量未指定" + }, + { + "name": "牛奶一盒 约", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶一盒 约 250ml", + "notes": "量未指定" + }, + { + "name": "蔬菜碎叶一把", + "quantity": null, + "unit": null, + "text_quantity": "- 蔬菜碎叶一把", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将牛奶与干燕麦混合搅拌均匀至黏稠状。" + }, + { + "step": 2, + "description": "将鸡蛋搅拌均匀至颜色单一程度。" + }, + { + "step": 3, + "description": "将鸡蛋液倒入燕麦牛奶中继续搅拌至黏稠、均匀。" + }, + { + "step": 4, + "description": "平底锅中加入一层黄油并覆盖均匀。" + }, + { + "step": 5, + "description": "下入搅拌好的食材,并摊开至饼状。" + }, + { + "step": 6, + "description": "小火加热两到三分钟。如想要加入蔬菜,可以在加热过程中加入碎菜叶。" + }, + { + "step": 7, + "description": "翻面继续加热两分钟。" + }, + { + "step": 8, + "description": "出锅,搭配剩下的牛奶作为早餐。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-牛奶燕麦", + "name": "牛奶燕麦的做法", + "description": "# 牛奶燕麦的做法\n\n高蛋白,粗谷物纤维,饱腹感的懒人快速营养早餐,3 分钟 搞定\n\n预估烹饪难度:★", + "source_path": "dishes/breakfast/牛奶燕麦.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 1, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛奶(巴氏奶口感更好)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶(巴氏奶口感更好)", + "notes": "量未指定" + }, + { + "name": "燕麦", + "quantity": null, + "unit": null, + "text_quantity": "- 燕麦", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "🥛 牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 🥛 牛奶 280ml/per", + "notes": "量未指定" + }, + { + "name": "🍳 鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 🍳 鸡蛋 1 个/per", + "notes": "量未指定" + }, + { + "name": "🍚 燕麦", + "quantity": null, + "unit": null, + "text_quantity": "- 🍚 燕麦 40g/per", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将牛奶倒入早餐杯(冷的即可)" + }, + { + "step": 2, + "description": "准备好 200ml 水,如果是直饮水直接加入燕麦,否则请烧开后加入燕麦" + }, + { + "step": 3, + "description": "水沸后 2 分钟,燕麦煮好" + }, + { + "step": 4, + "description": "煮好的燕麦捞出倒入牛奶中(尽量不要将煮燕麦的水也倒入牛奶,影响口感)" + }, + { + "step": 5, + "description": "将燕麦替换为快煮燕麦" + }, + { + "step": 6, + "description": "将牛奶倒入装有快煮燕麦的容器中并搅拌" + }, + { + "step": 7, + "description": "将混合物放入微波炉中" + }, + { + "step": 8, + "description": "中等火力微波 4 分钟" + }, + { + "step": 9, + "description": "热锅,锅内放一层底油,油热后煎鸡蛋,每面煎 20s,考虑调底味(3g 椒盐,可选)" + }, + { + "step": 10, + "description": "关火,装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-空气炸锅面包片", + "name": "空气炸锅面包片的做法", + "description": "# 空气炸锅面包片的做法\n\n健康饱肚子,适宜正在减脂期的程序员食用\n\n预估烹饪难度:★", + "source_path": "dishes/breakfast/空气炸锅面包片.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 1, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "面包片", + "quantity": null, + "unit": null, + "text_quantity": "- 面包片", + "notes": "量未指定" + }, + { + "name": "空气炸锅", + "quantity": null, + "unit": null, + "text_quantity": "- 空气炸锅", + "notes": "量未指定" + }, + { + "name": "面包片(两片)", + "quantity": null, + "unit": null, + "text_quantity": "- 面包片(两片)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "取出两片面包片(建议使用粗粮面包片)" + }, + { + "step": 2, + "description": "将面包片**垂直**放入空气炸锅" + }, + { + "step": 3, + "description": "200°C 烘烤 5 分钟" + }, + { + "step": 4, + "description": "取出即可使用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-美式炒蛋", + "name": "美式炒蛋的做法", + "description": "# 美式炒蛋的做法\n\n美式炒蛋具有松软鲜嫩的口感,与平时的炒蛋不同,美式炒蛋中加入了少量牛奶,使得蛋花更加的细密均匀,并且营养丰富~\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/美式炒蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "全脂牛奶/奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 全脂牛奶/奶油", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 3 个", + "notes": "量未指定" + }, + { + "name": "全脂牛奶/奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 全脂牛奶/奶油 10g", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 5 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋打入大碗中,加盐搅打至起泡,静置 15 分钟" + }, + { + "step": 2, + "description": "黄油切小块入锅,倒入蛋液,开小火不断搅拌" + }, + { + "step": 3, + "description": "黄油一融化,就快速翻动蛋液,将其打碎成细密状,在蛋液大体凝固前关火" + }, + { + "step": 4, + "description": "加入牛奶搅拌 15 秒,至炒蛋湿润绵密,装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-茶叶蛋", + "name": "茶叶蛋的做法", + "description": "# 茶叶蛋的做法\n\n茶香浓郁,鲜香可口的高蛋白快速营养早餐,大约耗时 30 分钟。烹饪略微耗时,可以周末尝试,做一次大约够 2-3 个人吃。\n\n预估烹饪难度:★★★", + "source_path": "dishes/breakfast/茶叶蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 3, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "茴香", + "quantity": null, + "unit": null, + "text_quantity": "- 茴香", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "红茶", + "quantity": null, + "unit": null, + "text_quantity": "- 红茶", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 400g(约 8 颗)", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 4g(约 2 颗)", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 0.5-1g(约 2 片)", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 3g(1 小块)", + "notes": "量未指定" + }, + { + "name": "茴香", + "quantity": null, + "unit": null, + "text_quantity": "- 茴香 5g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 15g", + "notes": "量未指定" + }, + { + "name": "红茶", + "quantity": null, + "unit": null, + "text_quantity": "- 红茶 20g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 25g", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 3g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "用冷水将鸡蛋煮熟,大火大约 8 分钟(根据自家厨具决定)" + }, + { + "step": 2, + "description": "鸡蛋捞出,过冷水" + }, + { + "step": 3, + "description": "将鸡蛋互相碰撞,使每个鸡蛋产生裂缝" + }, + { + "step": 4, + "description": "将鸡蛋下锅,放入八角,香叶,桂皮,茴香,冰糖,红茶,生抽,老抽,食盐" + }, + { + "step": 5, + "description": "加水直至没过鸡蛋" + }, + { + "step": 6, + "description": "大火煮开之后,转中小火煮 15 分钟" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-蒸水蛋", + "name": "蒸水蛋的做法", + "description": "# 蒸水蛋的做法\n\n蒸水蛋(北方有些地区叫鸡蛋糕儿)都是饭店的好吃,如何自己做水滑嫩香的蒸水蛋,本教程包教包会!\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/蒸水蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "新鲜鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜鸡蛋", + "notes": "量未指定" + }, + { + "name": "热水", + "quantity": null, + "unit": null, + "text_quantity": "- 热水", + "notes": "量未指定" + }, + { + "name": "锡纸或保鲜膜", + "quantity": null, + "unit": null, + "text_quantity": "- 锡纸或保鲜膜", + "notes": "量未指定" + }, + { + "name": "鸡蛋两只", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋两只", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2g", + "notes": "量未指定" + }, + { + "name": "热水", + "quantity": null, + "unit": null, + "text_quantity": "- 热水 260ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋打入碗中,打散" + }, + { + "step": 2, + "description": "取其他容器,倒入 1.5 倍(半个蛋壳为 0.5 倍水)于蛋液的温水(温度 20~30),将盐倒入水中化开" + }, + { + "step": 3, + "description": "将盐水倒入鸡蛋液中,顺时针或逆时针单方向搅拌均匀,气泡之类的可以用舀出丢弃,过筛则口感更加。" + }, + { + "step": 4, + "description": "使用锡纸包裹盛蛋液的碗(或用盘子盖住),置入提前带盖并加入大约 3cm 深度水的锅中" + }, + { + "step": 5, + "description": "中火烧至水开,转最小的火继续蒸 4 分钟" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-蒸花卷", + "name": "蒸花卷的做法", + "description": "# 蒸花卷的做法\n\n蒸花卷是一道简单易做的菜。能补充碳水化合物,膳食纤维。一般初学者只需要半小时即可完成。作为快手早餐,学会做之后,再也不会早上饿肚子了。\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/蒸花卷.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "冷冻花卷", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻花卷", + "notes": "量未指定" + }, + { + "name": "圆碟子", + "quantity": null, + "unit": null, + "text_quantity": "- 圆碟子", + "notes": "量未指定" + }, + { + "name": "蒸架", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸架", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 400ml", + "notes": "量未指定" + }, + { + "name": "冷冻花卷", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻花卷 5 个(女生分量 3 个即可)(可以在超市、各种买菜平台购买)", + "notes": "量未指定" + }, + { + "name": "圆碟子,直径", + "quantity": null, + "unit": null, + "text_quantity": "- 圆碟子,直径 28cm", + "notes": "量未指定" + }, + { + "name": "蒸架,直径", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸架,直径 20cm", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 400ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "从花卷的包装袋中取出 5 个花卷" + }, + { + "step": 2, + "description": "把花卷平铺在碟子上,尽量不用重叠" + }, + { + "step": 3, + "description": "往锅里倒入 400ml 水,把蒸架放里面,把装花卷的碟子放在蒸架上,盖上锅盖。" + }, + { + "step": 4, + "description": "开大火加热,直至水沸腾。" + }, + { + "step": 5, + "description": "转中火加热 15 分钟" + }, + { + "step": 6, + "description": "开盖用手感受花卷的表面温度,如果不够热,就继续盖上盖子加热,否则就可以关火出锅。" + }, + { + "step": 7, + "description": "碟子取出放凉至 50 度即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-蛋煎糍粑", + "name": "蛋煎糍粑的做法", + "description": "# 蛋煎糍粑的做法\n\n蛋煎糍粑做法很简单,不需要太多的厨艺基础~\n\n蛋煎糍粑热量高,美味+顶饿+便宜,只需十分钟就可以完成~\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/蛋煎糍粑.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "糍粑", + "quantity": null, + "unit": null, + "text_quantity": "- 糍粑", + "notes": "量未指定" + }, + { + "name": "白糖或红糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖或红糖", + "notes": "量未指定" + }, + { + "name": "糍粑 两块", + "quantity": null, + "unit": null, + "text_quantity": "- 糍粑 两块", + "notes": "量未指定" + }, + { + "name": "红糖", + "quantity": null, + "unit": null, + "text_quantity": "- 红糖 10g (建议 8g - 15g 之间)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "把糍粑切成长方形小块,便于后面煎" + }, + { + "step": 2, + "description": "碗里打入一个鸡蛋并把鸡蛋搅碎,加入 2g 食用盐" + }, + { + "step": 3, + "description": "将切好的小糍粑依此放入搅碎的鸡蛋里面,涂抹完糍粑双面为止" + }, + { + "step": 4, + "description": "锅里倒入植物油 10ml ,把涂抹好的糍粑小块放进去小火慢慢煎软。" + }, + { + "step": 5, + "description": "将剩下的鸡蛋液慢慢倒在糍粑表面" + }, + { + "step": 6, + "description": "用筷子或者勺子为糍粑翻面,来回煎至金黄色后开吃" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-金枪鱼酱三明治", + "name": "金枪鱼酱三明治的做法", + "description": "# 金枪鱼酱三明治的做法\n\n饱腹感很强的懒人早餐,营养很丰富,高蛋白,大概 5 分钟搞定。可以配着牛奶、咖啡等饮品一起吃。\n\n预估烹饪难度:★", + "source_path": "dishes/breakfast/金枪鱼酱三明治.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 1, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "水浸金枪鱼罐头(不建议用油浸,会很腻)", + "quantity": null, + "unit": null, + "text_quantity": "- 水浸金枪鱼罐头(不建议用油浸,会很腻)", + "notes": "量未指定" + }, + { + "name": "方形吐司片", + "quantity": null, + "unit": null, + "text_quantity": "- 方形吐司片", + "notes": "量未指定" + }, + { + "name": "蛋黄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋黄酱", + "notes": "量未指定" + }, + { + "name": "俄式酸黄瓜汁", + "quantity": null, + "unit": null, + "text_quantity": "- 俄式酸黄瓜汁", + "notes": "量未指定" + }, + { + "name": "芝士片(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 芝士片(可选)", + "notes": "量未指定" + }, + { + "name": "火腿片(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿片(可选)", + "notes": "量未指定" + }, + { + "name": "轻食机", + "quantity": null, + "unit": null, + "text_quantity": "- 轻食机", + "notes": "量未指定" + }, + { + "name": "水浸金枪鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 水浸金枪鱼 65g", + "notes": "量未指定" + }, + { + "name": "方形吐司片", + "quantity": null, + "unit": null, + "text_quantity": "- 方形吐司片 2 片", + "notes": "量未指定" + }, + { + "name": "蛋黄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋黄酱 50 mL", + "notes": "量未指定" + }, + { + "name": "俄式酸黄瓜汁", + "quantity": null, + "unit": null, + "text_quantity": "- 俄式酸黄瓜汁 10-15mL(可根据个人口味调整)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将金枪鱼、蛋黄酱、俄式酸黄瓜汁倒入碗中,用勺子搅拌,保证将金枪鱼块搅碎,酱整体呈糊状,并备用" + }, + { + "step": 2, + "description": "将 1 片吐司放在轻食机上" + }, + { + "step": 3, + "description": "将做好的金枪鱼酱涂抹到吐司上,建议 10-15ml" + }, + { + "step": 4, + "description": "将另一片方形吐司片覆盖在上面,并按压轻食机,开机" + }, + { + "step": 5, + "description": "待轻食机自动停止加热,即可装盘使用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-鸡蛋三明治", + "name": "鸡蛋三明治的做法", + "description": "# 鸡蛋三明治的做法\n\n10 分钟的简易鸡蛋三明治 🥪\n\n预估烹饪难度:★★", + "source_path": "dishes/breakfast/鸡蛋三明治.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 2, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "吐司", + "quantity": null, + "unit": null, + "text_quantity": "- 吐司", + "notes": "量未指定" + }, + { + "name": "培根", + "quantity": null, + "unit": null, + "text_quantity": "- 培根", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "蛋黄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋黄酱", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "黑胡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "吐司", + "quantity": null, + "unit": null, + "text_quantity": "- 吐司 2 片", + "notes": "量未指定" + }, + { + "name": "培根", + "quantity": null, + "unit": null, + "text_quantity": "- 培根 2 片", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 10 g", + "notes": "量未指定" + }, + { + "name": "蛋黄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋黄酱 20g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1g", + "notes": "量未指定" + }, + { + "name": "黑胡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "吐司切去四边,备用" + }, + { + "step": 2, + "description": "鸡蛋煮熟,捣碎" + }, + { + "step": 3, + "description": "混合鸡蛋、蛋黄酱、盐、黑胡椒" + }, + { + "step": 4, + "description": "锅中加入黄油,煎熟培根" + }, + { + "step": 5, + "description": "组装吐司,在两片吐司间加入制作好的鸡蛋酱及培根" + }, + { + "step": 6, + "description": "四边形吐司切成三角形装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-温泉蛋-温泉蛋", + "name": "温泉蛋的做法", + "description": "# 温泉蛋的做法\n\n一种传统的日式小吃,可以用于各种佐餐,注意与溏心蛋区分,溏心蛋是蛋黄不熟蛋白熟了,温泉蛋是蛋白不熟蛋黄熟了\n\n预估烹饪难度:★★★", + "source_path": "dishes/breakfast/温泉蛋/温泉蛋.md", + "image_path": null, + "images": [], + "category": "早餐", + "difficulty": 3, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [], + "steps": [ + { + "step": 1, + "description": "在锅中盛装一定量自来水,确保水面没过约鸡蛋 3cm,水中插入温度计" + }, + { + "step": 2, + "description": "开火或打开电磁炉,逐渐调整电磁炉功率或火苗大小,使得水温保持在 **70 摄氏度**" + }, + { + "step": 3, + "description": "将鸡蛋放入锅中。鸡蛋不可互相堆叠,应皆在底部,并留有空间可以晃动" + }, + { + "step": 4, + "description": "保持当前温度 **25 分钟**" + }, + { + "step": 5, + "description": "准备一杯冰水" + }, + { + "step": 6, + "description": "捞出鸡蛋,并立刻放入冰水中,**等待 3 分钟**" + }, + { + "step": 7, + "description": "将鸡蛋打入小碗,完成制作" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-breakfast-苏格兰蛋-苏格兰蛋", + "name": "苏格兰蛋的做法", + "description": "\n\n\n\n# 苏格兰蛋的做法\n\n\n\n\n\n![简易版苏格兰蛋](./egg1.png)\n\n苏格兰蛋是一种用新鲜肉糜裹住鸡蛋,放入油中炸至金黄制成,这个版本比较费事,所以在此就给大家带来简易版,苏格兰蛋复杂版大家就自行查找。\n\n简易版苏格兰蛋是利用手抓饼皮包裹住芝士培根糖心蛋放入油中炸至金黄制成,大约耗时 20-30 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/breakfast/苏格兰蛋/苏格兰蛋.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg1.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg1.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg2.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg3.png" + ], + "category": "早餐", + "difficulty": 3, + "tags": [ + "早餐" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "手抓饼皮", + "quantity": null, + "unit": null, + "text_quantity": "- 手抓饼皮", + "notes": "量未指定" + }, + { + "name": "芝士", + "quantity": null, + "unit": null, + "text_quantity": "- 芝士", + "notes": "量未指定" + }, + { + "name": "培根", + "quantity": null, + "unit": null, + "text_quantity": "- 培根", + "notes": "量未指定" + }, + { + "name": "空气炸锅或者油锅", + "quantity": null, + "unit": null, + "text_quantity": "- 空气炸锅或者油锅", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 50g(约 1 颗)", + "notes": "量未指定" + }, + { + "name": "手抓饼", + "quantity": null, + "unit": null, + "text_quantity": "- 手抓饼 1 份-2 份(看鸡蛋大小)", + "notes": "量未指定" + }, + { + "name": "芝士片", + "quantity": null, + "unit": null, + "text_quantity": "- 芝士片 1-2 片", + "notes": "量未指定" + }, + { + "name": "培根片", + "quantity": null, + "unit": null, + "text_quantity": "- 培根片 1-2 片", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "用冷水下锅水开 3 分钟后捞出" + }, + { + "step": 2, + "description": "鸡蛋捞出,放入冰水中剥壳更快速也更完整" + }, + { + "step": 3, + "description": "用芝士片包裹鸡蛋" + }, + { + "step": 4, + "description": "培根片包裹鸡蛋" + }, + { + "step": 5, + "description": "手抓饼两端切除以矩形包裹鸡蛋" + }, + { + "step": 6, + "description": "油温 6 成下锅(油面波动,有青烟,筷子插入油中周围泛起气泡即是 6 成温度) 炸制金黄即可" + }, + { + "step": 7, + "description": "空气炸锅 160 度 15 分钟" + }, + { + "step": 8, + "description": "切开即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-油酥", + "name": "油酥的做法", + "description": "# 油酥的做法\n\n油酥是由面粉与热油混合调制的,通常在烙饼时涂点油酥,可以使得饼子层层分明,外酥里软,口感更佳。\n\n预估烹饪难度:★★", + "source_path": "dishes/condiment/油酥.md", + "image_path": null, + "images": [], + "category": "调料", + "difficulty": 2, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油 = (要烙饼的张数", + "quantity": null, + "unit": null, + "text_quantity": "- 油 = (要烙饼的张数 * 10ml)", + "notes": "量未指定" + }, + { + "name": "盐 = (要烙饼的张数 /", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = (要烙饼的张数 / 2)g", + "notes": "量未指定" + }, + { + "name": "面粉 = (要烙饼的张数 /", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 = (要烙饼的张数 / 0.13)g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "面粉盛小碗里,加入盐" + }, + { + "step": 2, + "description": "加入 200 度的热油" + }, + { + "step": 3, + "description": "用筷子将其搅拌成无固状物体的糊状。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-炸串酱料", + "name": "炸串酱料的做法", + "description": "# 炸串酱料的做法\n\n炸串酱料,号称淋袜子都好吃,新手友好,预计用时 10 分钟。\n\n预估烹饪难度:★★", + "source_path": "dishes/condiment/炸串酱料.md", + "image_path": null, + "images": [], + "category": "调料", + "difficulty": 2, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "干辣椒面(粗细都准备)", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒面(粗细都准备)", + "notes": "量未指定" + }, + { + "name": "孜然粉", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然粉", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "花椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒粉", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香", + "notes": "量未指定" + }, + { + "name": "麻辣鲜", + "quantity": null, + "unit": null, + "text_quantity": "- 麻辣鲜", + "notes": "量未指定" + }, + { + "name": "白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻", + "notes": "量未指定" + }, + { + "name": "干辣椒面", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒面 60 克", + "notes": "量未指定" + }, + { + "name": "孜然粉", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然粉 20 克", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 10 克", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 15 克", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 20 克", + "notes": "量未指定" + }, + { + "name": "花椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒粉 15 克", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 8 克", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香 5 克", + "notes": "量未指定" + }, + { + "name": "麻辣鲜", + "quantity": null, + "unit": null, + "text_quantity": "- 麻辣鲜 5 克", + "notes": "量未指定" + }, + { + "name": "白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻 30 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "所有原料在容器内混合,搅拌均匀。" + }, + { + "step": 2, + "description": "锅里烧热油,油的用量以在容器内没过所有原材料为佳。" + }, + { + "step": 3, + "description": "分三次淋入热油,每次 1/3,同时搅拌。" + }, + { + "step": 4, + "description": "最后放入香油 10ml,生抽 10ml,花椒油 10ml,蚝油 10ml。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-简易版炒糖色", + "name": "简易版炒糖色的做法", + "description": "# 简易版炒糖色的做法\n\n这是简易的糖色的做法。对于更为进阶的技巧和糖色更为进阶的用法,请学习[糖色的炒制](../../tips/advanced/糖色的炒制.md)。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/condiment/简易版炒糖色.md", + "image_path": null, + "images": [], + "category": "调料", + "difficulty": 4, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "糖(任选其一):", + "quantity": null, + "unit": null, + "text_quantity": "- 糖(任选其一):", + "notes": "量未指定" + }, + { + "name": "冰糖:炒出来的`糖色`色泽最为鲜艳,红亮,必须水油炒,不加水融化会很慢", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖:炒出来的`糖色`色泽最为鲜艳,红亮,必须水油炒,不加水融化会很慢", + "notes": "量未指定" + }, + { + "name": "白砂糖:必须水油炒,不加水融化会很慢", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖:必须水油炒,不加水融化会很慢", + "notes": "量未指定" + }, + { + "name": "绵白糖:可以不加水", + "quantity": null, + "unit": null, + "text_quantity": "- 绵白糖:可以不加水", + "notes": "量未指定" + }, + { + "name": "炒糖色过程火不要太大!!!电磁炉温度不够,火候过了发苦,不够发甜", + "quantity": null, + "unit": null, + "text_quantity": "- 炒糖色过程火不要太大!!!电磁炉温度不够,火候过了发苦,不够发甜", + "notes": "量未指定" + }, + { + "name": "`油`:100ml", + "quantity": null, + "unit": null, + "text_quantity": "- `油`:100ml", + "notes": "量未指定" + }, + { + "name": "`开水`:500ml", + "quantity": null, + "unit": null, + "text_quantity": "- `开水`:500ml", + "notes": "量未指定" + }, + { + "name": "`糖`(这里以冰糖为例)", + "quantity": null, + "unit": null, + "text_quantity": "- `糖`(这里以冰糖为例)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "开火,并向锅中倒入 100ml 开水" + }, + { + "step": 2, + "description": "再向锅中倒入 100ml 油,与第一步间隔越短越好,此时锅为大火中火都可以,着急的话可以大火" + }, + { + "step": 3, + "description": "放入冰糖(如果冰糖过于耦合,可以提前敲碎,做到耦合度越低越好)" + }, + { + "step": 4, + "description": "调整火力为中火" + }, + { + "step": 5, + "description": "开始搅拌" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-糖醋汁", + "name": "糖醋汁的做法", + "description": "# 糖醋汁的做法\n\n糖醋汁通常情况下由清水、白糖、白醋等制成,有些人喜欢放一些番茄酱来增添不一样的酸甜味或放一些淀粉来增加菜肴汤汁的粘性和浓度,糖醋汁可用于糖醋鱼、糖醋里脊、糖醋排骨等菜品的制作\n\n可依据糖醋汁配制的经典比例 1:2:3:4:5 来调制糖醋汁\n\n预估烹饪难度:★★", + "source_path": "dishes/condiment/糖醋汁.md", + "image_path": null, + "images": [], + "category": "调料", + "difficulty": 2, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "白醋/米醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋/米醋", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "清水(50ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 清水(50ml)", + "notes": "量未指定" + }, + { + "name": "生抽(40ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽(40ml)", + "notes": "量未指定" + }, + { + "name": "白糖(30g)", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖(30g)", + "notes": "量未指定" + }, + { + "name": "白醋(20ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋(20ml)", + "notes": "量未指定" + }, + { + "name": "料酒(10ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒(10ml)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "按照比例将各调料在小碗中搅拌均匀" + }, + { + "step": 2, + "description": "按不同菜肴的方式处理完毕后,将配制好的糖醋汁倒入锅中" + }, + { + "step": 3, + "description": "根据各菜肴的不同,烹制 5-10 分钟" + }, + { + "step": 4, + "description": "大火收汁,可增加菜的浓度、香味和光泽" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-葱油", + "name": "葱油的做法", + "description": "# 葱油的做法\n\n葱油是用热油萃取以葱为主的各类香辛料得到的产物,可以用来调制肉馅,做凉拌菜,在热炒菜中作为出锅明油使用。\n\n预估烹饪难度:★★★", + "source_path": "dishes/condiment/葱油.md", + "image_path": null, + "images": [], + "category": "调料", + "difficulty": 3, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "葱(大葱小葱都可以)", + "quantity": null, + "unit": null, + "text_quantity": "- 葱(大葱小葱都可以)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "香菜(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜(可选)", + "notes": "量未指定" + }, + { + "name": "开洋(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 开洋(可选)", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 200g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 80g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 20g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10ml", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 150g", + "notes": "量未指定" + }, + { + "name": "开洋", + "quantity": null, + "unit": null, + "text_quantity": "- 开洋 50g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "开洋泡入 50 度温水中,加入 10ml 料酒去腥,泡 10 分钟后取出沥干水分" + }, + { + "step": 2, + "description": "葱,香菜洗净,切成 5cm 长的段,擦干表面水份" + }, + { + "step": 3, + "description": "洋葱切成丝,在锅里用热水煮 5 分钟,取出沥干水份" + }, + { + "step": 4, + "description": "姜去皮,切成片" + }, + { + "step": 5, + "description": "锅里倒入全部油,放入上述预处理好的材料,开中小火炸 20 分钟" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-蒜香酱油", + "name": "蒜香酱油的做法", + "description": "# 蒜香酱油的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/condiment/蒜香酱油.md", + "image_path": null, + "images": [], + "category": "调料", + "difficulty": 2, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻", + "notes": "量未指定" + }, + { + "name": "花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "蘸料碟", + "quantity": null, + "unit": null, + "text_quantity": "- 蘸料碟", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 2 瓣", + "notes": "量未指定" + }, + { + "name": "白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻 5 克", + "notes": "量未指定" + }, + { + "name": "花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油 15 毫升", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 30 毫升", + "notes": "量未指定" + }, + { + "name": "蘸料碟", + "quantity": null, + "unit": null, + "text_quantity": "- 蘸料碟 1 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "拍碎蒜头" + }, + { + "step": 2, + "description": "往蘸料碟中加入酱油" + }, + { + "step": 3, + "description": "起锅,加入花生油,等到油温滚烫后加入拍好的蒜头,炸半分钟" + }, + { + "step": 4, + "description": "半分钟后,关火,把热油倒入蘸料碟,用筷子搅拌即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-油泼辣子-油泼辣子", + "name": "油泼辣子的做法", + "description": "# 油泼辣子的做法\n\n![image](./口水鸡+油泼辣子.jpg)\n![image](./油泼辣子.jpg)\n\n制作耗时 10 分钟\n\n预估烹饪难度:★★★", + "source_path": "dishes/condiment/油泼辣子/油泼辣子.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/油泼辣子/口水鸡+油泼辣子.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/油泼辣子/口水鸡+油泼辣子.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/油泼辣子/油泼辣子.jpg" + ], + "category": "调料", + "difficulty": 3, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "干辣椒面", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒面", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "熟白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 熟白芝麻", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "花生油(可用菜籽油替换)", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油(可用菜籽油替换)", + "notes": "量未指定" + }, + { + "name": "家庭小陶瓷碗", + "quantity": null, + "unit": null, + "text_quantity": "- 家庭小陶瓷碗", + "notes": "量未指定" + }, + { + "name": "家庭铁勺子", + "quantity": null, + "unit": null, + "text_quantity": "- 家庭铁勺子", + "notes": "量未指定" + }, + { + "name": "五香粉 (可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 (可选)", + "notes": "量未指定" + }, + { + "name": "草寇(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 草寇(可选)", + "notes": "量未指定" + }, + { + "name": "小葱 (可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 (可选)", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "白芷", + "quantity": null, + "unit": null, + "text_quantity": "- 白芷", + "notes": "量未指定" + }, + { + "name": "姜片(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片(可选)", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 1 个", + "notes": "量未指定" + }, + { + "name": "干辣椒面", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒面 100 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5 克", + "notes": "量未指定" + }, + { + "name": "熟白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 熟白芝麻 15 克", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 1 个", + "notes": "量未指定" + }, + { + "name": "花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油 150 毫升 (可用菜籽油替换)", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 10 克(可选)", + "notes": "量未指定" + }, + { + "name": "草寇", + "quantity": null, + "unit": null, + "text_quantity": "- 草寇 1 个(可选)", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 3-5 根(可选)", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 30 克", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋 5 ml(大概就是小铁勺子的量)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "拿出蒜头掰 2 个`小蒜头`去皮" + }, + { + "step": 2, + "description": "拿出砧板剁碎`小蒜头`、`小米椒`" + }, + { + "step": 3, + "description": "拿出碗倒入`花生油`" + }, + { + "step": 4, + "description": "油热放入`其他配料`和`小葱`,等到香料变焦,捞出扔掉" + }, + { + "step": 5, + "description": "拿出铁锅将碗内的油放入加热 2 分钟(菜籽油烧至冒烟)" + }, + { + "step": 6, + "description": "此时是空碗" + }, + { + "step": 7, + "description": "往空碗加入`干辣椒面`、`白芝麻`、`蒜末`、`小米椒`、`盐`、`五香粉`、`草寇`作为\"调料\"" + }, + { + "step": 8, + "description": "关火将油温冷却至 `210` 摄氏度" + }, + { + "step": 9, + "description": "将锅内热油倒入碗内并用勺子搅拌即可(可以在 `165` 摄氏度时加入同样\"调料\"的碗最后进行混合进行增辣)" + }, + { + "step": 10, + "description": "倒入热油稍微搅拌后放入白醋,此时会重新沸腾。继续进行搅拌,白醋增香。" + }, + { + "step": 11, + "description": "油泼辣子冷却到温热放白糖和味精,白糖可以是辣味柔和,不会那么的呛口" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-草莓酱-草莓酱", + "name": "草莓酱的做法", + "description": "# 草莓酱的做法\n\n可以买那种一筐一筐卖的小草莓,主要是便宜。做成酱抹在面包上非常好吃。\n\n预估烹饪难度:★★", + "source_path": "dishes/condiment/草莓酱/草莓酱.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/做好的草莓酱.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/做好的草莓酱.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/洗好的草莓.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/混合好的草莓.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/熬煮的草莓.jpeg" + ], + "category": "调料", + "difficulty": 2, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "草莓", + "quantity": null, + "unit": null, + "text_quantity": "- 草莓", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "保鲜膜", + "quantity": null, + "unit": null, + "text_quantity": "- 保鲜膜", + "notes": "量未指定" + }, + { + "name": "草莓", + "quantity": null, + "unit": null, + "text_quantity": "- 草莓 1200 克", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 400 克 (如需要低糖饮食,可以考虑降低到 200g)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "草莓洗净去叶" + }, + { + "step": 2, + "description": "将草莓切碎放入合适的碗中" + }, + { + "step": 3, + "description": "将白糖倒入碗中与草莓搅拌均匀" + }, + { + "step": 4, + "description": "碗用保鲜膜覆盖静置 1 小时" + }, + { + "step": 5, + "description": "将静置的草莓和糖的混合物倒入不粘锅中开大火烧开" + }, + { + "step": 6, + "description": "烧开后转小火不断搅拌直至果酱呈粘稠状关火" + }, + { + "step": 7, + "description": "待草莓酱冷却后装入准备好的密封罐中" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-condiment-蔗糖糖浆-蔗糖糖浆", + "name": "蔗糖糖浆的做法", + "description": "# 蔗糖糖浆的做法\n\n将糖事先溶解好便于在配制饮料(特别是冷饮)时给饮料增甜\n\n预估烹饪难度:★", + "source_path": "dishes/condiment/蔗糖糖浆/蔗糖糖浆.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/蔗糖糖浆/bottle.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/蔗糖糖浆/bottle.jpg" + ], + "category": "调料", + "difficulty": 1, + "tags": [ + "调料" + ], + "servings": 1, + "ingredients": [ + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "可密封容器(建议使用高硼硅试剂瓶,便宜)", + "quantity": null, + "unit": null, + "text_quantity": "- 可密封容器(建议使用高硼硅试剂瓶,便宜)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 100 克", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 100 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-反沙芋头-反沙芋头", + "name": "反沙芋头的做法", + "description": "# 反沙芋头的做法\n\n![反沙芋头成品](./反沙芋头成品.jpg)\n\n反沙芋头是一道著名的潮汕小吃,下午茶,制作起来特别方便,~预计制作时间 20 分钟\n\n预估烹饪难度:★★★", + "source_path": "dishes/dessert/反沙芋头/反沙芋头.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/反沙芋头/反沙芋头成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/反沙芋头/反沙芋头成品.jpg" + ], + "category": "甜品", + "difficulty": 3, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "荔浦芋头(电商平台购买即可,实惠新鲜)", + "quantity": null, + "unit": null, + "text_quantity": "- 荔浦芋头(电商平台购买即可,实惠新鲜)", + "notes": "量未指定" + }, + { + "name": "白砂糖或冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖或冰糖", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "荔浦芋头", + "quantity": null, + "unit": null, + "text_quantity": "- 荔浦芋头 200g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 30g", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 15g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "芋头切长条(稍微大条一点,翻炒过程不容易烂)" + }, + { + "step": 2, + "description": "加入可以没过芋头的油,等油温起来(插入筷子冒小泡即可)" + }, + { + "step": 3, + "description": "放进芋头到油里,去炸到芋头浮起来,一般是微微泛黄并且可以用筷子很轻松戳洞" + }, + { + "step": 4, + "description": "炸芋头的油放起来别浪费,后面炒菜啥的都能用" + }, + { + "step": 5, + "description": "接下来关键的一步,把糖(30g)和水(15g)按照 2:1 比例,加热至不变色且冒小泡" + }, + { + "step": 6, + "description": "倒入葱花和芋头,关火翻炒,此时等温度下来,糖就会有反沙的效果" + }, + { + "step": 7, + "description": "装盘上桌!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-咖啡椰奶冻-咖啡椰奶冻", + "name": "咖啡椰奶冻的做法", + "description": "# 咖啡椰奶冻的做法\n\n![咖啡椰奶冻](./咖啡椰奶冻.png)\n\n咖啡椰奶冻是一道简单易于制作的甜品 出品时间约 1 小时(不算冷藏)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/dessert/咖啡椰奶冻/咖啡椰奶冻.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/咖啡椰奶冻/咖啡椰奶冻.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/咖啡椰奶冻/咖啡椰奶冻.png" + ], + "category": "甜品", + "difficulty": 4, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "125ml 淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 125ml 淡奶油", + "notes": "量未指定" + }, + { + "name": "250ml 椰树牌椰汁", + "quantity": null, + "unit": null, + "text_quantity": "- 250ml 椰树牌椰汁", + "notes": "量未指定" + }, + { + "name": "35ml espresso 意式浓缩", + "quantity": null, + "unit": null, + "text_quantity": "- 35ml espresso 意式浓缩", + "notes": "量未指定" + }, + { + "name": "50ml 椰子水", + "quantity": null, + "unit": null, + "text_quantity": "- 50ml 椰子水", + "notes": "量未指定" + }, + { + "name": "10g 吉利丁(gelatin)", + "quantity": null, + "unit": null, + "text_quantity": "- 10g 吉利丁(gelatin)", + "notes": "量未指定" + }, + { + "name": "过滤网(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 过滤网(可选)", + "notes": "量未指定" + }, + { + "name": "(那个...有摆盘需求的话,可以来点蓝莓 and/or 咖啡粉)", + "quantity": null, + "unit": null, + "text_quantity": "- (那个...有摆盘需求的话,可以来点蓝莓 and/or 咖啡粉)", + "notes": "量未指定" + }, + { + "name": "125ml 淡奶油(whipping cream,", + "quantity": null, + "unit": null, + "text_quantity": "- 125ml 淡奶油(whipping cream, 35% M.E)", + "notes": "量未指定" + }, + { + "name": "250ml 椰树牌椰汁", + "quantity": null, + "unit": null, + "text_quantity": "- 250ml 椰树牌椰汁", + "notes": "量未指定" + }, + { + "name": "35ml espresso 意式浓缩(个人不推荐用 fruity 的咖啡豆)", + "quantity": null, + "unit": null, + "text_quantity": "- 35ml espresso 意式浓缩(个人不推荐用 fruity 的咖啡豆)", + "notes": "量未指定" + }, + { + "name": "50ml 椰子水(推荐 vita coco coconut water,有条件可以敲一个椰子)", + "quantity": null, + "unit": null, + "text_quantity": "- 50ml 椰子水(推荐 vita coco coconut water,有条件可以敲一个椰子)", + "notes": "量未指定" + }, + { + "name": "10g 吉利丁", + "quantity": null, + "unit": null, + "text_quantity": "- 10g 吉利丁", + "notes": "量未指定" + }, + { + "name": "糖(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 糖(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将定量淡奶油,椰树牌椰汁,espresso,椰子水混合备用。" + }, + { + "step": 2, + "description": "将以上液体加热 1 分钟,温度达到 50-60 度即可。" + }, + { + "step": 3, + "description": "(可选)如果格外嗜甜可以加额外的糖。" + }, + { + "step": 4, + "description": "倒入吉利丁,搅拌至融化,煮 1 分钟。" + }, + { + "step": 5, + "description": "(可选)过筛 (这一步可以让椰奶冻口感更佳顺畅)。" + }, + { + "step": 6, + "description": "放入模具。" + }, + { + "step": 7, + "description": "(可选)过滤掉表层的泡泡。这一步可以让椰奶冻口感更好,并且看着也会更棒。" + }, + { + "step": 8, + "description": "放入冰箱冷藏区,等待 3 小时。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-奥利奥冰淇淋-奥利奥冰淇淋", + "name": "奥利奥冰淇淋的做法", + "description": "# 奥利奥冰淇淋的做法\n\n奥利奥冰淇淋是简单但好吃的冰淇淋,纯动物奶油不腻口,预计制作时长半小时(主要消耗在搅打奶油和去除奥利奥夹心上)。\n\n预估烹饪难度:★★★", + "source_path": "dishes/dessert/奥利奥冰淇淋/奥利奥冰淇淋.md", + "image_path": null, + "images": [], + "category": "甜品", + "difficulty": 3, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "淡奶油(推荐品牌 安佳动物淡奶油)", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油(推荐品牌 安佳动物淡奶油)", + "notes": "量未指定" + }, + { + "name": "原味奥利奥", + "quantity": null, + "unit": null, + "text_quantity": "- 原味奥利奥", + "notes": "量未指定" + }, + { + "name": "电动打蛋器", + "quantity": null, + "unit": null, + "text_quantity": "- 电动打蛋器", + "notes": "量未指定" + }, + { + "name": "小刀(或者可以去除夹心的工具)", + "quantity": null, + "unit": null, + "text_quantity": "- 小刀(或者可以去除夹心的工具)", + "notes": "量未指定" + }, + { + "name": "冰淇淋模具(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 冰淇淋模具(可选)", + "notes": "量未指定" + }, + { + "name": "奥利奥", + "quantity": null, + "unit": null, + "text_quantity": "- 奥利奥 6 块", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 18 克", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油 250 毫升", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将奥利奥拧开后去除利利(夹心),备用" + }, + { + "step": 2, + "description": "用筷子将奥奥剁碎,需要有一半奥奥变成粉状,另一半的奥奥最大长度小于 0.5 厘米,备用(某宝可搜“奥利奥饼干碎”,节省时间精力^-^)" + }, + { + "step": 3, + "description": "将奶油全部倒置于深容器中,并加入准备好的糖" + }, + { + "step": 4, + "description": "开始用电动打蛋器高速挡 搅打至 电动打蛋器提起后下方会出现**悬挂住**的奶油( 0.5 厘米 - 1 厘米),而不是**全部**像液体一样滴下(部分滴下是正常现象)。" + }, + { + "step": 5, + "description": "搅打完成后将奥奥放入奶油中,搅拌均匀直至底部有奥奥。" + }, + { + "step": 6, + "description": "可选:将混合物倒入冰淇淋模具中" + }, + { + "step": 7, + "description": "放置冰箱冷冻室( -18 度) 4 小时以上可取出" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-戚风蛋糕-戚风蛋糕", + "name": "戚风蛋糕的做法", + "description": "# 戚风蛋糕的做法\n\n戚风蛋糕是一道烘焙入门菜品,有一定操作难度。但成功制作后,其口感细腻绵软,令人回味。加上烘烤时间,一般初学者需要 **1.5 - 2 小时**即可完成。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/dessert/戚风蛋糕/戚风蛋糕.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08606.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08606.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08608.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08612.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08618.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08621.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08627.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08716.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/IMG_0269.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/IMG_1516.jpg" + ], + "category": "甜品", + "difficulty": 5, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "1 个鸡蛋(正常中等大小,约", + "quantity": null, + "unit": null, + "text_quantity": "- 1 个鸡蛋(正常中等大小,约 50g)", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 16g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 8g", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 10g", + "notes": "量未指定" + }, + { + "name": "低筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 低筋面粉 17g", + "notes": "量未指定" + }, + { + "name": "6 寸:大小为", + "quantity": null, + "unit": null, + "text_quantity": "- 6 寸:大小为 3 份(即三个鸡蛋)。面积 36 个单位。", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 3 个,白糖 50g,食用油 25g,牛奶 30g,低筋面粉 50g", + "notes": "量未指定" + }, + { + "name": "8 寸:大小为", + "quantity": null, + "unit": null, + "text_quantity": "- 8 寸:大小为 5 份(即五个鸡蛋)。面积 64 个单位。", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 5 个,白糖 80g,食用油 40g,牛奶 50g,低筋面粉 90g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "牛奶(或水)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶(或水)", + "notes": "量未指定" + }, + { + "name": "食用油(或黄油,但需加热软化)", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油(或黄油,但需加热软化)", + "notes": "量未指定" + }, + { + "name": "低筋面粉(推荐惠宜)", + "quantity": null, + "unit": null, + "text_quantity": "- 低筋面粉(推荐惠宜)", + "notes": "量未指定" + }, + { + "name": "[可选] 柠檬汁或白醋", + "quantity": null, + "unit": null, + "text_quantity": "- [可选] 柠檬汁或白醋", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "(可选) 将模具从高处落下,震出其中的热气" + }, + { + "step": 2, + "description": "模具倒扣 10 分钟,使蛋糕冷却" + }, + { + "step": 3, + "description": "脱模,食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-提拉米苏-提拉米苏", + "name": "提拉米苏的做法", + "description": "# 提拉米苏的做法\n\n![自家提拉米苏成品](提拉米苏成品.jpg)\n\n提拉米苏,是意大利传统甜品。无需烤箱操作简便,烘焙新手也可以零失误获得一份美味的提拉米苏。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/dessert/提拉米苏/提拉米苏.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/提拉米苏/提拉米苏成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/提拉米苏/提拉米苏成品.jpg" + ], + "category": "甜品", + "difficulty": 4, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "马斯卡彭芝士", + "quantity": null, + "unit": null, + "text_quantity": "- 马斯卡彭芝士", + "notes": "量未指定" + }, + { + "name": "手指饼干", + "quantity": null, + "unit": null, + "text_quantity": "- 手指饼干", + "notes": "量未指定" + }, + { + "name": "放凉浓缩咖啡", + "quantity": null, + "unit": null, + "text_quantity": "- 放凉浓缩咖啡", + "notes": "量未指定" + }, + { + "name": "无菌鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 无菌鸡蛋", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "可可粉", + "quantity": null, + "unit": null, + "text_quantity": "- 可可粉", + "notes": "量未指定" + }, + { + "name": "朗姆酒(不喜欢酒的朋友可省略,可按照自己口味调节)", + "quantity": null, + "unit": null, + "text_quantity": "- 朗姆酒(不喜欢酒的朋友可省略,可按照自己口味调节)", + "notes": "量未指定" + }, + { + "name": "一个装成品的容器(这里用的是玻璃乐扣)", + "quantity": null, + "unit": null, + "text_quantity": "- 一个装成品的容器(这里用的是玻璃乐扣)", + "notes": "量未指定" + }, + { + "name": "打蛋器(手劲儿大的朋友也可以锻炼臂力)", + "quantity": null, + "unit": null, + "text_quantity": "- 打蛋器(手劲儿大的朋友也可以锻炼臂力)", + "notes": "量未指定" + }, + { + "name": "马斯卡彭芝士", + "quantity": null, + "unit": null, + "text_quantity": "- 马斯卡彭芝士 450g", + "notes": "量未指定" + }, + { + "name": "手指饼干", + "quantity": null, + "unit": null, + "text_quantity": "- 手指饼干 1 包", + "notes": "量未指定" + }, + { + "name": "放凉浓缩咖啡", + "quantity": null, + "unit": null, + "text_quantity": "- 放凉浓缩咖啡 350ml", + "notes": "量未指定" + }, + { + "name": "无菌鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 无菌鸡蛋 4 个", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 50g", + "notes": "量未指定" + }, + { + "name": "可可粉", + "quantity": null, + "unit": null, + "text_quantity": "- 可可粉 10g", + "notes": "量未指定" + }, + { + "name": "朗姆酒", + "quantity": null, + "unit": null, + "text_quantity": "- 朗姆酒 35ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "分离蛋黄蛋清" + }, + { + "step": 2, + "description": "盛有蛋白的碗中加 10g 白砂糖湿性打发" + }, + { + "step": 3, + "description": "盛有蛋黄的碗中将 40g 白砂糖分三次加入,搅拌至均匀" + }, + { + "step": 4, + "description": "蛋黄中分三次加入马斯卡彭芝士,搅拌至均匀" + }, + { + "step": 5, + "description": "蛋黄中最后加入朗姆酒,搅拌均匀" + }, + { + "step": 6, + "description": "将打发好的蛋白分三次加入蛋黄芝士液中" + }, + { + "step": 7, + "description": "手指饼干两面浸湿咖啡液,平铺入容器" + }, + { + "step": 8, + "description": "两层芝士液两层饼干交替放入容器(这一步按照大家意愿及容器高度酌情处理)" + }, + { + "step": 9, + "description": "放入冰箱冷藏四个小时(心急的小伙伴可以提早拿出来)" + }, + { + "step": 10, + "description": "取出后在表面筛上可可粉,即可享用啦" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-无厨师机蜂蜜面包-无厨师机蜂蜜面包", + "name": "无厨师机蜂蜜面包的做法", + "description": "# 无厨师机蜂蜜面包的做法\n\n![自家成品](./无厨师机蜂蜜面包.jpg)\n\n这个菜谱不需要厨师机,只需要等待!可以晚上的时候准备好放入冰箱,第二天再烤。口感虽然不如使用厨师机的但是还行,冰箱保存要吃的时候微波炉叮一下更好。花费时间大多在发面。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/dessert/无厨师机蜂蜜面包/无厨师机蜂蜜面包.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/无厨师机蜂蜜面包/无厨师机蜂蜜面包.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/无厨师机蜂蜜面包/无厨师机蜂蜜面包.jpg" + ], + "category": "甜品", + "difficulty": 5, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "高筋面粉:400g", + "quantity": null, + "unit": null, + "text_quantity": "- 高筋面粉:400g", + "notes": "量未指定" + }, + { + "name": "牛奶:", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶: 200g", + "notes": "量未指定" + }, + { + "name": "酵母:4g", + "quantity": null, + "unit": null, + "text_quantity": "- 酵母:4g", + "notes": "量未指定" + }, + { + "name": "鸡蛋:1 个", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋:1 个", + "notes": "量未指定" + }, + { + "name": "白砂糖:70g", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖:70g", + "notes": "量未指定" + }, + { + "name": "盐:", + "quantity": null, + "unit": null, + "text_quantity": "- 盐: 2g", + "notes": "量未指定" + }, + { + "name": "黄油:", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油: 30g", + "notes": "量未指定" + }, + { + "name": "蜂蜜:20g", + "quantity": null, + "unit": null, + "text_quantity": "- 蜂蜜:20g", + "notes": "量未指定" + }, + { + "name": "水:", + "quantity": null, + "unit": null, + "text_quantity": "- 水: 20g", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "制作面团:将面粉,牛奶(建议加热到 40°,本人使用微波炉 15 - 20s),酵母,鸡蛋,面粉,糖和盐混合起来。" + }, + { + "step": 2, + "description": "搅拌面团,将原料混合均匀成团。" + }, + { + "step": 3, + "description": "加入黄油混合。" + }, + { + "step": 4, + "description": "继续搅拌 + 手揉,均匀混合。" + }, + { + "step": 5, + "description": "开始发面,使用保鲜膜覆盖容器,普通气温(10 - 20°)放置 1 - 2 小时,稍长时间对效果影响不大。" + }, + { + "step": 6, + "description": "明显看到面团发酵变大(2 倍)即可开始切分面团, 此时面团应该不再十分黏手。" + }, + { + "step": 7, + "description": "切分面团:理想状态每一份 60g(美观),可根据喜好适当调整大小。" + }, + { + "step": 8, + "description": "将每一份小面团使用擀面杖擀成舌状后卷起, 再次醒面 10 分钟左右。" + }, + { + "step": 9, + "description": "再次使用擀面杖擀成舌状后卷起, 从中间切开(一个变成两个)。" + }, + { + "step": 10, + "description": "再次使用擀面杖擀成舌状后卷起, 从中间切开(两个变成四个)。(此步骤可以按照自己的时间多擀/卷几次, 把握一份的大小就行)" + }, + { + "step": 11, + "description": "烤盘放入油纸并倒入花生油, (每份卷好的)底部蘸水 + 面粉后放入烤盘。" + }, + { + "step": 12, + "description": "再次醒发(盖上保鲜膜), 这一步可以放入冰箱, 第二天再烤。" + }, + { + "step": 13, + "description": "刷上蛋液。" + }, + { + "step": 14, + "description": "烤箱 180°(355°F), 18 - 20 分钟。" + }, + { + "step": 15, + "description": "出炉后, 刷上蜂蜜水, 撒上芝麻。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-炸鲜奶-炸鲜奶", + "name": "炸鲜奶的做法", + "description": "# 炸鲜奶的做法\n\n![炸鲜奶成品](./炸鲜奶.jpg)\n\n炸鲜奶是一种外脆里嫩的甜点,营养价值适中,制作难度中等,预计制作时长约为 20 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/dessert/炸鲜奶/炸鲜奶.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/炸鲜奶/炸鲜奶.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/炸鲜奶/炸鲜奶.jpg" + ], + "category": "甜品", + "difficulty": 3, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉", + "notes": "量未指定" + }, + { + "name": "面包糠", + "quantity": null, + "unit": null, + "text_quantity": "- 面包糠", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "面包模具(或浅盘子)", + "quantity": null, + "unit": null, + "text_quantity": "- 面包模具(或浅盘子)", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 250g", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉 30g", + "notes": "量未指定" + }, + { + "name": "面包糠", + "quantity": null, + "unit": null, + "text_quantity": "- 面包糠 100g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 30g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将牛奶倒入碗中" + }, + { + "step": 2, + "description": "加入玉米淀粉和白糖,搅拌均匀" + }, + { + "step": 3, + "description": "将模具刷上食用油" + }, + { + "step": 4, + "description": "牛奶下锅,中火烧开" + }, + { + "step": 5, + "description": "烧开后转小火,边煮边搅拌" + }, + { + "step": 6, + "description": "牛奶*变粘稠*后出锅,倒入模具" + }, + { + "step": 7, + "description": "将模具放冰箱**冷却 1 小时**" + }, + { + "step": 8, + "description": "拿出,切成大小均匀的条,随后放入碗中" + }, + { + "step": 9, + "description": "向碗中倒入一半的面包糠,奶糊裹上后取出,备用" + }, + { + "step": 10, + "description": "在一个新碗中打入鸡蛋,搅匀,备用" + }, + { + "step": 11, + "description": "将奶糊裹上蛋液和剩余的面包糠" + }, + { + "step": 12, + "description": "锅中倒入足以覆盖奶糊的油,下锅" + }, + { + "step": 13, + "description": "奶糊外观*呈金黄状态*后停火,摆盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-烤蛋挞-烤蛋挞", + "name": "烤蛋挞的做法", + "description": "# 烤蛋挞的做法\n\n![烤蛋挞](./烤蛋挞.png)\n\n烤蛋挞是一道简单易于制作的甜品 且半成品可置于冰箱冷冻长时间保存 随吃随取 出品时间约 1 小时\n\n预估烹饪难度:★★★★", + "source_path": "dishes/dessert/烤蛋挞/烤蛋挞.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/烤蛋挞/烤蛋挞.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/烤蛋挞/烤蛋挞.png" + ], + "category": "甜品", + "difficulty": 4, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "蛋挞皮 品牌不限", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋挞皮 品牌不限", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "烤箱 大小不限", + "quantity": null, + "unit": null, + "text_quantity": "- 烤箱 大小不限", + "notes": "量未指定" + }, + { + "name": "克数称", + "quantity": null, + "unit": null, + "text_quantity": "- 克数称", + "notes": "量未指定" + }, + { + "name": "搅拌器 包含且不限于筷子 打蛋器等工具", + "quantity": null, + "unit": null, + "text_quantity": "- 搅拌器 包含且不限于筷子 打蛋器等工具", + "notes": "量未指定" + }, + { + "name": "筛网 网孔约为", + "quantity": null, + "unit": null, + "text_quantity": "- 筛网 网孔约为 1 毫米", + "notes": "量未指定" + }, + { + "name": "蛋挞皮 品牌不限 整包蛋挞皮约为", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋挞皮 品牌不限 整包蛋挞皮约为 30 只", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 8 个 普通鸡蛋即可", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 200 毫升 普通袋装牛奶即可", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油 450 毫升 烘焙店或超市即有售", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 80 克 普通砂糖即可 细砂糖更优 易于融化", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将碗置于克数称上 称量 450 克 淡奶油(淡奶油密度在此处记为 1 )" + }, + { + "step": 2, + "description": "加入 80 克白砂糖 (甜度中等 可按个人口味增减 建议范围 60-100 克)" + }, + { + "step": 3, + "description": "加入 200 克牛奶 (牛奶密度在此处记为 1 )" + }, + { + "step": 4, + "description": "取 8 个蛋黄加入 蛋清可留作他用" + }, + { + "step": 5, + "description": "均匀搅拌所有材料直至白砂糖全部融化" + }, + { + "step": 6, + "description": "使用网筛对搅拌完成的食材进行过滤 滤除鸡蛋黏膜 鸡蛋壳 未融化的白砂糖 结块的淡奶油" + }, + { + "step": 7, + "description": "此时请将烤箱设置 220 摄氏度开始预热(约 10 分钟) 记得拿出烤盘" + }, + { + "step": 8, + "description": "将蛋挞皮以 0.5 厘米的间隔均匀放置于烤盘中" + }, + { + "step": 9, + "description": "将过滤完成的食材倒入蛋挞皮中 液面距离蛋挞皮上沿 0.5 厘米即可不宜过多" + }, + { + "step": 10, + "description": "截止此步骤 半成品蛋挞的制作已经完成 可直接放入冰箱速冻 12 小时以上保存" + }, + { + "step": 11, + "description": "将半成品蛋挞放入烤箱中进行烤制 温度为 200 摄氏度 时间为 25 分钟" + }, + { + "step": 12, + "description": "烤制结束后即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-玛格丽特饼干-玛格丽特饼干", + "name": "玛格丽特饼干的做法", + "description": "# 玛格丽特饼干的做法\n\n![玛格丽特成品](./玛格丽特饼干.jpg)\n\n玛格丽特饼干通常作为下午茶点心或伴随热饮享用,是一种经典而受欢迎的点心。它们的酥脆质地和丰富的黄油味道使它们成为许多人喜爱的饼干之一。\n\n预估烹饪难度:★★★", + "source_path": "dishes/dessert/玛格丽特饼干/玛格丽特饼干.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/玛格丽特饼干/玛格丽特饼干.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/玛格丽特饼干/玛格丽特饼干.jpg" + ], + "category": "甜品", + "difficulty": 3, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "熟蛋黄", + "quantity": null, + "unit": null, + "text_quantity": "- 熟蛋黄", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "低筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 低筋面粉", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉", + "notes": "量未指定" + }, + { + "name": "烤箱", + "quantity": null, + "unit": null, + "text_quantity": "- 烤箱", + "notes": "量未指定" + }, + { + "name": "熟蛋黄", + "quantity": null, + "unit": null, + "text_quantity": "- 熟蛋黄 1 个", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 50 克", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 20 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1 克", + "notes": "量未指定" + }, + { + "name": "低筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 低筋面粉 50 克", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉 50 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "黄油隔热水融化、将蛋黄磨碎备用。" + }, + { + "step": 2, + "description": "在融化的黄油中添加糖、盐、以及碾碎的鸡蛋黄,搅拌均匀" + }, + { + "step": 3, + "description": "加入低筋面粉与玉米淀粉,揉成面团" + }, + { + "step": 4, + "description": "将面团均匀分割成大约 8 克重的小面团,然后将它们搓成球状。" + }, + { + "step": 5, + "description": "使用大拇指轻压在每个小面团上,以形成裂纹。" + }, + { + "step": 6, + "description": "预热烤箱至 150℃,将小面团放入烤箱中,烘烤 20 分钟。" + }, + { + "step": 7, + "description": "微微放凉即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-红柚蛋糕-红柚蛋糕", + "name": "红柚蛋糕的做法", + "description": "# 红柚蛋糕的做法\n\n红柚蛋糕是空气炸锅基础甜点,一份适合单人食用,食材处理需要 10 分钟,烹饪需要 25 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/dessert/红柚蛋糕/红柚蛋糕.md", + "image_path": null, + "images": [], + "category": "甜品", + "difficulty": 3, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "空气炸锅", + "quantity": null, + "unit": null, + "text_quantity": "- 空气炸锅", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "红柚果肉", + "quantity": null, + "unit": null, + "text_quantity": "- 红柚果肉", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "锡纸盘", + "quantity": null, + "unit": null, + "text_quantity": "- 锡纸盘", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 80g", + "notes": "量未指定" + }, + { + "name": "红柚果肉", + "quantity": null, + "unit": null, + "text_quantity": "- 红柚果肉 20g", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 15ml", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 80ml", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 15g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锡纸盘里打入鸡蛋 2 个, 加入红柚果肉 20g" + }, + { + "step": 2, + "description": "锡纸盘中倒入 15ml 油并摇晃锡纸盘时期均匀覆盖盘底" + }, + { + "step": 3, + "description": "锡纸盘中放入 10g 糖, 以及 40g 面粉和 40ml 水" + }, + { + "step": 4, + "description": "用筷子顺时针方向搅拌至淡黄色糊状" + }, + { + "step": 5, + "description": "锡纸盘中放入 5g 糖, 以及 40g 面粉和 40ml 水" + }, + { + "step": 6, + "description": "继续用筷子搅拌至淡黄色糊状" + }, + { + "step": 7, + "description": "锡纸盘放入空气炸锅的烤篮上,用 180 摄氏度烤 15 分钟" + }, + { + "step": 8, + "description": "打开空气炸锅,小心取出锡纸盘,用筷子或勺子将蛋糕翻面" + }, + { + "step": 9, + "description": "继续 180 摄氏度烤 8 分钟" + }, + { + "step": 10, + "description": "取出即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-芋泥雪媚娘-芋泥雪媚娘", + "name": "芋泥雪媚娘的做法", + "description": "# 芋泥雪媚娘的做法\n\n![芋泥雪媚娘成品](./芋泥雪媚娘成品.jpg)\n\n芋泥雪媚娘是一道甜品,很适合做给孩子吃,无需烤箱,手残党也可以做成功~预计制作时间 2 小时。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/dessert/芋泥雪媚娘/芋泥雪媚娘.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/芋泥雪媚娘/芋泥雪媚娘成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/芋泥雪媚娘/芋泥雪媚娘成品.jpg" + ], + "category": "甜品", + "difficulty": 5, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "荔浦芋头(电商平台购买即可,实惠新鲜)", + "quantity": null, + "unit": null, + "text_quantity": "- 荔浦芋头(电商平台购买即可,实惠新鲜)", + "notes": "量未指定" + }, + { + "name": "紫薯粉", + "quantity": null, + "unit": null, + "text_quantity": "- 紫薯粉", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "糯米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米粉", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "料理搅拌机(电动打蛋器也可以)", + "quantity": null, + "unit": null, + "text_quantity": "- 料理搅拌机(电动打蛋器也可以)", + "notes": "量未指定" + }, + { + "name": "筛网", + "quantity": null, + "unit": null, + "text_quantity": "- 筛网", + "notes": "量未指定" + }, + { + "name": "保鲜膜", + "quantity": null, + "unit": null, + "text_quantity": "- 保鲜膜", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "荔浦芋头", + "quantity": null, + "unit": null, + "text_quantity": "- 荔浦芋头 200g", + "notes": "量未指定" + }, + { + "name": "紫薯粉", + "quantity": null, + "unit": null, + "text_quantity": "- 紫薯粉 3g", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 165g", + "notes": "量未指定" + }, + { + "name": "糯米粉 a", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米粉 a 50g", + "notes": "量未指定" + }, + { + "name": "糯米粉 b", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米粉 b 75g", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉 22g", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 30g", + "notes": "量未指定" + }, + { + "name": "淡奶油(推荐安佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油(推荐安佳) 145g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 26g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "芋头切块,大火煮熟至软(40 分钟即可),全部放入料理机" + }, + { + "step": 2, + "description": "向内加入 30g 牛奶,25g 淡奶油,将其打成泥状" + }, + { + "step": 3, + "description": "再向内加入 3g 紫薯粉,18g 白砂糖,继续搅拌打成细腻芋泥" + }, + { + "step": 4, + "description": "取出另一个碗,加入全部糯米粉 b,22g 玉米淀粉,135g 牛奶,50g 白砂糖,混匀并过筛一遍,保鲜膜盖上并扎小洞,中火蒸半个小时" + }, + { + "step": 5, + "description": "在蒸的过程中,将糯米粉 a 放入平底锅小火翻炒至微微发黄(即炒熟),作为手粉备用" + }, + { + "step": 6, + "description": "将中火蒸完半小时的糯米牛奶混合物(果冻状)趁热加入黄油 30g,将黄油揉至面团完全吸收,然后放冰箱冷藏一小时" + }, + { + "step": 7, + "description": "取出另一只碗,加入 120g 淡奶油,8g 白砂糖,打发至有纹路,装进裱花袋备用" + }, + { + "step": 8, + "description": "取出冷藏后的面团,搓揉 5 分钟,分成 30g 一个,均匀撒上 2g 手粉防粘,擀成圆形,先挤上 5g 裱花奶油,然后放上 30g 芋泥,最后将面饼像包包子一样包起来(可以减去多余的皮)" + }, + { + "step": 9, + "description": "包好后再均匀撒 2g 手粉防粘" + }, + { + "step": 10, + "description": "重复以上两步直至原材料用光" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-英式司康-英式司康", + "name": "英式司康的做法", + "description": "# 英式司康的做法\n\n![示例菜成品](./英式司康.png)\n\n英式司康是非常简单快手的下午茶甜品,可以搭配果酱、茶与咖啡。成品以蛋奶香气为主轴风味,糖量适中不会过于甜腻。\n\n预估烹饪难度:★★★", + "source_path": "dishes/dessert/英式司康/英式司康.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/英式司康/英式司康.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/英式司康/英式司康.png" + ], + "category": "甜品", + "difficulty": 3, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "无盐黄油(推荐品牌总统)", + "quantity": null, + "unit": null, + "text_quantity": "- 无盐黄油(推荐品牌总统)", + "notes": "量未指定" + }, + { + "name": "低筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 低筋面粉", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "泡打粉", + "quantity": null, + "unit": null, + "text_quantity": "- 泡打粉", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油", + "notes": "量未指定" + }, + { + "name": "奶油奶酪(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 奶油奶酪(可选)", + "notes": "量未指定" + }, + { + "name": "无盐黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 无盐黄油 40g", + "notes": "量未指定" + }, + { + "name": "低筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 低筋面粉 180g", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 30g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1g", + "notes": "量未指定" + }, + { + "name": "泡打粉", + "quantity": null, + "unit": null, + "text_quantity": "- 泡打粉 5g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个(约 50g)", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油 45g", + "notes": "量未指定" + }, + { + "name": "奶油奶酪", + "quantity": null, + "unit": null, + "text_quantity": "- 奶油奶酪 50g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋打散,称量出 30g 蛋液放入干净容器中,放入全量淡奶油和奶油奶酪混合均匀。如果奶酪太硬可以水浴加热至大约 40 度再混合。" + }, + { + "step": 2, + "description": "将低筋面粉,盐,糖,泡打粉放入干净容器中混合均匀" + }, + { + "step": 3, + "description": "黄油切成小块,放入上一步的混合物中,用手将黄油捏入混合物中,呈粗玉米粉质地" + }, + { + "step": 4, + "description": "将第一步的蛋奶混合液倒入上一步得到的粉油混合物种,搅拌均接近。叠压成均匀面团" + }, + { + "step": 5, + "description": "面团放到案板上,擀成 1.5cm 厚的面片,用刀或者模具分切成合适的形状" + }, + { + "step": 6, + "description": "用刷子蘸取剩余的 20g 鸡蛋液,刷在司康表面" + }, + { + "step": 7, + "description": "烤箱预热 180 度,烤制 27 分钟" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-草莓冰淇淋-草莓冰淇淋", + "name": "草莓冰淇淋的做法", + "description": "# 草莓冰淇淋的做法\n\n草莓冰淇淋是简单但好吃的冰淇淋,可以做很多不同的口味。这次将用当季的新鲜草莓制作美味,**不需要搅拌**的草莓冰淇淋。\n\n预估烹饪难度:★★", + "source_path": "dishes/dessert/草莓冰淇淋/草莓冰淇淋.md", + "image_path": null, + "images": [], + "category": "甜品", + "difficulty": 2, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "加糖炼乳", + "quantity": null, + "unit": null, + "text_quantity": "- 加糖炼乳", + "notes": "量未指定" + }, + { + "name": "草莓", + "quantity": null, + "unit": null, + "text_quantity": "- 草莓", + "notes": "量未指定" + }, + { + "name": "重奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 重奶油", + "notes": "量未指定" + }, + { + "name": "香草精", + "quantity": null, + "unit": null, + "text_quantity": "- 香草精", + "notes": "量未指定" + }, + { + "name": "冰淇淋模具(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 冰淇淋模具(可选)", + "notes": "量未指定" + }, + { + "name": "草莓糖浆:", + "quantity": null, + "unit": null, + "text_quantity": "- 草莓糖浆:", + "notes": "量未指定" + }, + { + "name": "草莓", + "quantity": null, + "unit": null, + "text_quantity": "- 草莓 500 克", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 45 克", + "notes": "量未指定" + }, + { + "name": "香草精", + "quantity": null, + "unit": null, + "text_quantity": "- 香草精 1 克", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 1 克", + "notes": "量未指定" + }, + { + "name": "冰淇淋底料:", + "quantity": null, + "unit": null, + "text_quantity": "- 冰淇淋底料:", + "notes": "量未指定" + }, + { + "name": "香草精", + "quantity": null, + "unit": null, + "text_quantity": "- 香草精 5 克", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 1 克", + "notes": "量未指定" + }, + { + "name": "重奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 重奶油 6 克", + "notes": "量未指定" + }, + { + "name": "加糖炼乳", + "quantity": null, + "unit": null, + "text_quantity": "- 加糖炼乳 400 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "先做草莓糖浆。把草莓洗干净,去掉顶部叶子。将草莓切成 **5mm** 的小块。保留一半切碎的草莓,稍后折叠成冰淇淋。" + }, + { + "step": 2, + "description": "将另一半切碎的草莓和糖一起放入酱汁锅中。用中火搅拌和烹饪,直到草莓释放液体并在锅中形成糖浆。" + }, + { + "step": 3, + "description": "让草莓在糖浆中加热,不时搅拌,直到它们分解并变形,糖浆稍微变稠。" + }, + { + "step": 4, + "description": "当糖浆保持分开 **3秒钟** 时,就已经准备好了。把糖浆从火上移开,加入香草和盐搅拌。将草莓糖浆放在一边冷却。" + }, + { + "step": 5, + "description": "当糖浆冷却时,准备冰淇淋基料。在碗中加入甜炼乳、浓奶油、香草精和盐。使用手动搅拌器搅打混合物,直到它变得轻盈蓬松,并形成柔软的尖峰。" + }, + { + "step": 6, + "description": "将保留的切碎的新鲜草莓折叠到冰淇淋底座中。将生过的冰淇淋底座转移到冷冻安全容器中。将冷却的草莓糖浆淋在冰淇淋上,然后轻轻地将其旋入混合物中。" + }, + { + "step": 7, + "description": "盖上冰淇淋并冷冻 **八小时** ,然后舀取和食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-酸奶意式奶冻-酸奶意式奶冻", + "name": "酸奶意式奶冻的做法", + "description": "# 酸奶意式奶冻的做法\n\n![示例菜成品](./酸奶意式奶冻.png)\n\n意式奶冻非常适合作为餐后甜品,可以搭配果酱、水果和香草。成品增加了原味酸奶,不会过于甜腻。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/dessert/酸奶意式奶冻/酸奶意式奶冻.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/酸奶意式奶冻/酸奶意式奶冻.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/酸奶意式奶冻/酸奶意式奶冻.png" + ], + "category": "甜品", + "difficulty": 4, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "原味酸奶", + "quantity": null, + "unit": null, + "text_quantity": "- 原味酸奶", + "notes": "量未指定" + }, + { + "name": "吉利丁片", + "quantity": null, + "unit": null, + "text_quantity": "- 吉利丁片", + "notes": "量未指定" + }, + { + "name": "筛网", + "quantity": null, + "unit": null, + "text_quantity": "- 筛网", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油 200g", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 40g", + "notes": "量未指定" + }, + { + "name": "原味酸奶", + "quantity": null, + "unit": null, + "text_quantity": "- 原味酸奶 250g", + "notes": "量未指定" + }, + { + "name": "吉利丁片", + "quantity": null, + "unit": null, + "text_quantity": "- 吉利丁片 6g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "吉利丁片剪成小片,泡入冷水中" + }, + { + "step": 2, + "description": "淡奶油和糖放入锅中,加热至 60 度" + }, + { + "step": 3, + "description": "关火,吉利丁从水中取出,控干水份,加入热淡奶油中,搅拌均匀" + }, + { + "step": 4, + "description": "淡奶油降温至 40 度,加入原味酸奶,搅拌均匀" + }, + { + "step": 5, + "description": "将上述步骤得到的混合物过两遍筛网" + }, + { + "step": 6, + "description": "分装入合适的容器,放入冰箱冷藏 4 小时以上" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-雪花酥-雪花酥", + "name": "雪花酥的做法", + "description": "# 雪花酥的做法\n\n![雪花酥成品](./雪花酥成品.jpg)\n\n雪花酥是一个快捷简便的甜点,适合装盒送礼,制作耗时 30 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/dessert/雪花酥/雪花酥.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/雪花酥/雪花酥成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/雪花酥/雪花酥成品.jpg" + ], + "category": "甜品", + "difficulty": 3, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "无盐黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 无盐黄油 20g", + "notes": "量未指定" + }, + { + "name": "棉花糖", + "quantity": null, + "unit": null, + "text_quantity": "- 棉花糖 75g", + "notes": "量未指定" + }, + { + "name": "全脂奶粉", + "quantity": null, + "unit": null, + "text_quantity": "- 全脂奶粉 40g", + "notes": "量未指定" + }, + { + "name": "混合坚果", + "quantity": null, + "unit": null, + "text_quantity": "- 混合坚果 60g", + "notes": "量未指定" + }, + { + "name": "饼干", + "quantity": null, + "unit": null, + "text_quantity": "- 饼干 75g", + "notes": "量未指定" + }, + { + "name": "无盐黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 无盐黄油", + "notes": "量未指定" + }, + { + "name": "棉花糖", + "quantity": null, + "unit": null, + "text_quantity": "- 棉花糖", + "notes": "量未指定" + }, + { + "name": "全脂奶粉", + "quantity": null, + "unit": null, + "text_quantity": "- 全脂奶粉", + "notes": "量未指定" + }, + { + "name": "混合坚果(三只松鼠每日坚果)", + "quantity": null, + "unit": null, + "text_quantity": "- 混合坚果(三只松鼠每日坚果)", + "notes": "量未指定" + }, + { + "name": "饼干(非夹心型饼干,推荐小奇福或购买使用雪花酥烘焙专用小饼干)", + "quantity": null, + "unit": null, + "text_quantity": "- 饼干(非夹心型饼干,推荐小奇福或购买使用雪花酥烘焙专用小饼干)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "饼干超过一元硬币大小先切成小块" + }, + { + "step": 2, + "description": "无盐黄油加入锅中,小火加热至无盐黄油完全融化" + }, + { + "step": 3, + "description": "棉花糖加入锅中,使用刮刀搅拌,直至棉花糖融化并与无盐黄油均匀融合" + }, + { + "step": 4, + "description": "20g 奶粉加入锅中,使用刮刀搅拌,奶粉与黄油棉花糖混合物搅拌均匀后立即关火" + }, + { + "step": 5, + "description": "准备好的所有混合坚果与饼干趁热加入锅中,使用刮刀搅拌" + }, + { + "step": 6, + "description": "搅拌到温度下降到手可以接触的温度后,戴上一次塑料手套,在锅中搓揉或者双手拿起拉扯,让饼干混合坚果与棉花糖黄油奶粉混合物分散均匀。" + }, + { + "step": 7, + "description": "将上述步骤混合物压入模具中,边角压实,擀面杖擀平,未满的一边用手尽量压成直边" + }, + { + "step": 8, + "description": "室温放凉,完全冷却后脱模,按照模具纹路切块,或切成自己喜欢的大小,撒上剩余奶粉,尽量使雪花酥每面都沾上奶粉" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-魔芋蛋糕-魔芋蛋糕", + "name": "魔芋蛋糕的做法", + "description": "# 魔芋蛋糕的做法\n\n魔芋蛋糕是一款低热量的甜点。蛋糕本身无麸质,并使用无热量的甜味剂代替白砂糖,非常适合减脂人群。加上烘烤时间,一般需要 **0.5 小时**即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/dessert/魔芋蛋糕/魔芋蛋糕.md", + "image_path": null, + "images": [], + "category": "甜品", + "difficulty": 4, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "3 个鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 3 个鸡蛋", + "notes": "量未指定" + }, + { + "name": "赤藓糖醇", + "quantity": null, + "unit": null, + "text_quantity": "- 赤藓糖醇 50g", + "notes": "量未指定" + }, + { + "name": "可可粉", + "quantity": null, + "unit": null, + "text_quantity": "- 可可粉 10g", + "notes": "量未指定" + }, + { + "name": "魔芋粉", + "quantity": null, + "unit": null, + "text_quantity": "- 魔芋粉 10g", + "notes": "量未指定" + }, + { + "name": "塔塔粉", + "quantity": null, + "unit": null, + "text_quantity": "- 塔塔粉 1g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "赤藓糖醇", + "quantity": null, + "unit": null, + "text_quantity": "- 赤藓糖醇", + "notes": "量未指定" + }, + { + "name": "可可粉", + "quantity": null, + "unit": null, + "text_quantity": "- 可可粉", + "notes": "量未指定" + }, + { + "name": "魔芋粉", + "quantity": null, + "unit": null, + "text_quantity": "- 魔芋粉", + "notes": "量未指定" + }, + { + "name": "塔塔粉", + "quantity": null, + "unit": null, + "text_quantity": "- 塔塔粉", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "在准备蛋糕糊之前,可以开始预热烤箱至 350 ℉( 150 ℃)以节省时间" + }, + { + "step": 2, + "description": "从冰箱中取出新鲜的鸡蛋" + }, + { + "step": 3, + "description": "准备两个容器并擦干,分别盛放蛋清与蛋黄" + }, + { + "step": 4, + "description": "对盛放蛋清的容器,可稍有水珠,但**不能有任何油**;盛放蛋黄的容器不能有水珠" + }, + { + "step": 5, + "description": "打蛋,手工或利用分蛋器,将蛋清与蛋黄分离到两个容器中。" + }, + { + "step": 6, + "description": "分离过程中蛋黄不能破碎,**蛋清中不能混有任何蛋黄**,否则会严重影响打发。(白色系带可进入蛋清,不影响)" + }, + { + "step": 7, + "description": "(注意,不使用厨房机的情况下,盛放蛋清的容器也是打蛋的容器,为避免溢出,加入全部蛋清后不要超过容器的 **1/8**)" + }, + { + "step": 8, + "description": "蛋清中加入 1g 塔塔粉" + }, + { + "step": 9, + "description": "打蛋器高速,打发蛋白至*粗大气泡的状态*,加入 50g 赤藓糖醇" + }, + { + "step": 10, + "description": "打蛋器中低速,打发蛋白至*干性发泡*的状态(提起打蛋器头,有短小直立的尖角;倒扣容器,蛋白可粘住容器不掉下来)" + }, + { + "step": 11, + "description": "此时蛋白打发程度已符合要求" + }, + { + "step": 12, + "description": "打蛋器应尽量贴近容器底部,防止出现上面浮着的表层打发,底部仍然是液体的情况)" + }, + { + "step": 13, + "description": "把分离的蛋黄加入打发的蛋清中,打蛋器中低速搅拌均匀" + }, + { + "step": 14, + "description": "蛋清中加入 **10g 可可粉**和 **10g 魔芋粉**,先用餐刀翻拌,这是由于如果直接用打蛋器搅拌会造成粉末飞溅" + }, + { + "step": 15, + "description": "翻拌手法是" + }, + { + "step": 16, + "description": "先用右手拿刮刀从搅拌盆中心插入面糊底部" + }, + { + "step": 17, + "description": "向 8 点钟方向刮去直到碰到盆壁,顺势舀起面糊提到空中,然后再移回盆中心将面糊放入盆内" + }, + { + "step": 18, + "description": "左手握住搅拌盆从 9 点钟方向转到 7 点钟方向,刚好旋转了 60 度,就完成了 1 次循环" + }, + { + "step": 19, + "description": "速度大约是 1 秒钟 2 下" + }, + { + "step": 20, + "description": "此方法出自《小岛老师的蛋糕教室》。用接地气的话说就是,像炒菜一样翻炒。" + }, + { + "step": 21, + "description": "翻拌结束后,打蛋器低速搅拌均匀" + }, + { + "step": 22, + "description": "模具铺好烘焙纸,贴合底部与内壁" + }, + { + "step": 23, + "description": "将蛋糕糊倒入模具,然后用餐刀抹平,再然后震荡几下避免大气泡" + }, + { + "step": 24, + "description": "烘烤 25 分钟" + }, + { + "step": 25, + "description": "烤好后,戴好隔热手套取出" + }, + { + "step": 26, + "description": "(可选) 将模具从高处落下,震出其中的热气" + }, + { + "step": 27, + "description": "模具倒扣 10 分钟,使蛋糕冷却" + }, + { + "step": 28, + "description": "没有冷却的蛋糕立刻脱模会损伤蛋糕" + }, + { + "step": 29, + "description": "此操作可能会**烫手**,注意戴好隔热手套" + }, + { + "step": 30, + "description": "脱模,餐刀切块食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-dessert-龟苓膏-龟苓膏", + "name": "龟苓膏的做法", + "description": "# 龟苓膏的做法\n\n![龟苓膏成品](./龟苓膏成品.jpg)\n\n预估烹饪难度:★★\n\n---", + "source_path": "dishes/dessert/龟苓膏/龟苓膏.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/龟苓膏/龟苓膏成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/龟苓膏/龟苓膏成品.jpg" + ], + "category": "甜品", + "difficulty": 2, + "tags": [ + "甜品" + ], + "servings": 1, + "ingredients": [ + { + "name": "龟苓膏粉", + "quantity": null, + "unit": null, + "text_quantity": "- 龟苓膏粉 25 克", + "notes": "量未指定" + }, + { + "name": "冷水", + "quantity": null, + "unit": null, + "text_quantity": "- 冷水 120 毫升", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 500 毫升", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 100 克", + "notes": "量未指定" + }, + { + "name": "小锅", + "quantity": null, + "unit": null, + "text_quantity": "- 小锅", + "notes": "量未指定" + }, + { + "name": "搅拌工具", + "quantity": null, + "unit": null, + "text_quantity": "- 搅拌工具", + "notes": "量未指定" + }, + { + "name": "模具或碗", + "quantity": null, + "unit": null, + "text_quantity": "- 模具或碗", + "notes": "量未指定" + }, + { + "name": "--", + "quantity": null, + "unit": null, + "text_quantity": "- --", + "notes": "量未指定" + }, + { + "name": "--", + "quantity": null, + "unit": null, + "text_quantity": "- --", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "--" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-B52轰炸机", + "name": "B52轰炸机的做法", + "description": "# B52轰炸机的做法\n\nB-52 是鸡尾酒中喝法比较独特的一种,要配上短吸管,餐巾纸和打火机。\n\n把酒点燃,用吸管一口气喝完,然后就能体验到先冷后热那种冰火两重天的感觉。那种感觉,只有试过才知道。\n\n用吸管适用于女士,最刺激的喝法是一口喝下,喝的时候注意尽量避免碰到杯口引起烫伤,让火在嘴里灭掉,才能喝出最好的味道。\n\n预估烹饪难度:★★★", + "source_path": "dishes/drink/B52轰炸机.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 3, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "甘露咖啡酒", + "quantity": null, + "unit": null, + "text_quantity": "- 甘露咖啡酒", + "notes": "量未指定" + }, + { + "name": "爱尔兰百利甜酒", + "quantity": null, + "unit": null, + "text_quantity": "- 爱尔兰百利甜酒", + "notes": "量未指定" + }, + { + "name": "蓝天原味伏特加", + "quantity": null, + "unit": null, + "text_quantity": "- 蓝天原味伏特加", + "notes": "量未指定" + }, + { + "name": "吧勺", + "quantity": null, + "unit": null, + "text_quantity": "- 吧勺", + "notes": "量未指定" + }, + { + "name": "利口酒杯", + "quantity": null, + "unit": null, + "text_quantity": "- 利口酒杯", + "notes": "量未指定" + }, + { + "name": "打火机", + "quantity": null, + "unit": null, + "text_quantity": "- 打火机", + "notes": "量未指定" + }, + { + "name": "甘露咖啡酒", + "quantity": null, + "unit": null, + "text_quantity": "- 甘露咖啡酒 10ml", + "notes": "量未指定" + }, + { + "name": "爱尔兰百利甜酒", + "quantity": null, + "unit": null, + "text_quantity": "- 爱尔兰百利甜酒 10ml", + "notes": "量未指定" + }, + { + "name": "蓝天原味伏特加", + "quantity": null, + "unit": null, + "text_quantity": "- 蓝天原味伏特加 10ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "在利口酒杯的最底层倒入甘露咖啡酒到 1/3 处。(10ml)" + }, + { + "step": 2, + "description": "顺着吧勺缓缓倒入爱尔兰百利甜酒,也是 1/3 处 (10ml)。注意要慢,保证层次分明。(太快甜酒会和咖啡混合)" + }, + { + "step": 3, + "description": "最后在上层倒入蓝天原味伏特加 (10ml)" + }, + { + "step": 4, + "description": "用打火机热一下杯口" + }, + { + "step": 5, + "description": "最后一步点火: 看到淡蓝色的小火苗了吗?" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-Mojito莫吉托", + "name": "Mojito莫吉托的做法", + "description": "# Mojito莫吉托的做法\n\nMojito 是一种传统的古巴高球鸡尾酒。\n\n这种调酒有着相对低的酒精含量(大约 10%)。\n\n预估烹饪难度:★★★", + "source_path": "dishes/drink/Mojito莫吉托.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 3, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "打碎的冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 打碎的冰块", + "notes": "量未指定" + }, + { + "name": "冰镇苏打水", + "quantity": null, + "unit": null, + "text_quantity": "- 冰镇苏打水", + "notes": "量未指定" + }, + { + "name": "压汁器", + "quantity": null, + "unit": null, + "text_quantity": "- 压汁器", + "notes": "量未指定" + }, + { + "name": "海波杯", + "quantity": null, + "unit": null, + "text_quantity": "- 海波杯", + "notes": "量未指定" + }, + { + "name": "研杵", + "quantity": null, + "unit": null, + "text_quantity": "- 研杵", + "notes": "量未指定" + }, + { + "name": "一块青柠(切成两个半块)", + "quantity": null, + "unit": null, + "text_quantity": "- 一块青柠(切成两个半块)", + "notes": "量未指定" + }, + { + "name": "五珠薄荷叶", + "quantity": null, + "unit": null, + "text_quantity": "- 五珠薄荷叶", + "notes": "量未指定" + }, + { + "name": "糖浆", + "quantity": null, + "unit": null, + "text_quantity": "- 糖浆 20ml", + "notes": "量未指定" + }, + { + "name": "金色朗姆酒", + "quantity": null, + "unit": null, + "text_quantity": "- 金色朗姆酒 45ml", + "notes": "量未指定" + }, + { + "name": "蓝天原味伏特加", + "quantity": null, + "unit": null, + "text_quantity": "- 蓝天原味伏特加 10ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将切成半块的青柠之一切成小块,放入海波杯,随后用研杵将其捣出汁;" + }, + { + "step": 2, + "description": "用 3-4 珠薄荷叶沿着杯口涂抹,随后将其放入杯中;" + }, + { + "step": 3, + "description": "加入 糖浆 20ml;" + }, + { + "step": 4, + "description": "加入 金色朗姆酒 45ml;" + }, + { + "step": 5, + "description": "将剩下的半块青柠压出汁水放入杯中;" + }, + { + "step": 6, + "description": "轻轻搅拌,使砂糖/糖浆处于半融合状态;" + }, + { + "step": 7, + "description": "将打碎的冰块放入杯中,直到占杯中 3/4;" + }, + { + "step": 8, + "description": "加入冰镇苏打水直到刚好淹没碎冰;" + }, + { + "step": 9, + "description": "旋转搅拌半分钟;" + }, + { + "step": 10, + "description": "使用碎冰将海波杯补满;" + }, + { + "step": 11, + "description": "将剩下的一株薄荷叶拍醒,插入碎冰,作装饰。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-冬瓜茶", + "name": "冬瓜茶的做法", + "description": "# 冬瓜茶的做法\n\n冬瓜茶是一种清爽的传统饮料,一般初学者需要 4~5 小时完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/冬瓜茶.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "冬瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 冬瓜", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "保鲜膜", + "quantity": null, + "unit": null, + "text_quantity": "- 保鲜膜", + "notes": "量未指定" + }, + { + "name": "过滤网", + "quantity": null, + "unit": null, + "text_quantity": "- 过滤网", + "notes": "量未指定" + }, + { + "name": "大锅", + "quantity": null, + "unit": null, + "text_quantity": "- 大锅", + "notes": "量未指定" + }, + { + "name": "冬瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 冬瓜 1000g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 300g", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-可乐桶", + "name": "可乐桶的做法", + "description": "# 可乐桶的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/可乐桶.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "波旁威士忌", + "quantity": null, + "unit": null, + "text_quantity": "- 波旁威士忌", + "notes": "量未指定" + }, + { + "name": "可口可乐", + "quantity": null, + "unit": null, + "text_quantity": "- 可口可乐", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "柠檬(可选,提升口感用)", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬(可选,提升口感用)", + "notes": "量未指定" + }, + { + "name": "手动压汁器", + "quantity": null, + "unit": null, + "text_quantity": "- 手动压汁器", + "notes": "量未指定" + }, + { + "name": "威士忌", + "quantity": null, + "unit": null, + "text_quantity": "- 威士忌 100 毫升", + "notes": "量未指定" + }, + { + "name": "可口可乐", + "quantity": null, + "unit": null, + "text_quantity": "- 可口可乐 500 毫升", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 1 个", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 300 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-奶茶", + "name": "奶茶的做法", + "description": "# 奶茶的做法\n\n奶茶是一种简单易做的饮料。一般初学者只需要 30 分钟即可完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/奶茶.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "袋泡红茶(推荐立顿黄牌精选红茶)", + "quantity": null, + "unit": null, + "text_quantity": "- 袋泡红茶(推荐立顿黄牌精选红茶)", + "notes": "量未指定" + }, + { + "name": "全脂奶粉或淡奶", + "quantity": null, + "unit": null, + "text_quantity": "- 全脂奶粉或淡奶", + "notes": "量未指定" + }, + { + "name": "杯子,例如带刻度的杯子,陶瓷杯或保温杯", + "quantity": null, + "unit": null, + "text_quantity": "- 杯子,例如带刻度的杯子,陶瓷杯或保温杯", + "notes": "量未指定" + }, + { + "name": "袋泡红茶", + "quantity": null, + "unit": null, + "text_quantity": "- 袋泡红茶 2 包(约 4g)", + "notes": "量未指定" + }, + { + "name": "奶粉", + "quantity": null, + "unit": null, + "text_quantity": "- 奶粉 11-12g", + "notes": "量未指定" + }, + { + "name": "砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 砂糖 5-7g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "取袋泡红茶 2 包放入杯中,加入 180-200mL **沸水**。" + }, + { + "step": 2, + "description": "**等待 20 - 30 分钟**。" + }, + { + "step": 3, + "description": "称取 11-12g 奶粉和 5-7g 砂糖,分别加入前一步骤得到的液体中。" + }, + { + "step": 4, + "description": "搅拌均匀即可饮用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-杨枝甘露", + "name": "杨枝甘露的做法", + "description": "# 杨枝甘露的做法\n\n没用西谷米的原因是家里没有,但是有很多的奇亚籽就拿来代替。而且奇亚籽用泡不用煮,省了很多时间!\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/杨枝甘露.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "杯子", + "quantity": null, + "unit": null, + "text_quantity": "- 杯子", + "notes": "量未指定" + }, + { + "name": "水果刀", + "quantity": null, + "unit": null, + "text_quantity": "- 水果刀", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "调理机/果汁机", + "quantity": null, + "unit": null, + "text_quantity": "- 调理机/果汁机", + "notes": "量未指定" + }, + { + "name": "奇亚籽", + "quantity": null, + "unit": null, + "text_quantity": "- 奇亚籽 24g", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 50ml", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 2 小块", + "notes": "量未指定" + }, + { + "name": "芒果", + "quantity": null, + "unit": null, + "text_quantity": "- 芒果 1 粒", + "notes": "量未指定" + }, + { + "name": "葡萄柚", + "quantity": null, + "unit": null, + "text_quantity": "- 葡萄柚 1/2 粒", + "notes": "量未指定" + }, + { + "name": "椰奶", + "quantity": null, + "unit": null, + "text_quantity": "- 椰奶 150ml", + "notes": "量未指定" + }, + { + "name": "切丝芒果干 (可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 切丝芒果干 (可选)", + "notes": "量未指定" + }, + { + "name": "切丝柳橙干 (可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 切丝柳橙干 (可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "奇亚籽泡牛奶 10 分钟。" + }, + { + "step": 2, + "description": "泡籽之时,把半粒芒果、葡萄柚去皮切丁,放入杯中。" + }, + { + "step": 3, + "description": "半粒芒果切小块放入调理机加冰块、椰奶打成泥。" + }, + { + "step": 4, + "description": "倒入杯中,放上点缀材料(如有)。" + }, + { + "step": 5, + "description": "一边享用一边写代码!!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-酸梅汤(半成品加工)", + "name": "酸梅汤(半成品加工)的做法", + "description": "# 酸梅汤(半成品加工)的做法\n\n预估烹饪难度:★", + "source_path": "dishes/drink/酸梅汤(半成品加工).md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 1, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "酸梅晶固体饮料", + "quantity": null, + "unit": null, + "text_quantity": "- 酸梅晶固体饮料", + "notes": "量未指定" + }, + { + "name": "方糖(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 方糖(可选)", + "notes": "量未指定" + }, + { + "name": "北京二锅头酒(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 北京二锅头酒(可选)", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 1177 克", + "notes": "量未指定" + }, + { + "name": "酸梅晶固体饮料", + "quantity": null, + "unit": null, + "text_quantity": "- 酸梅晶固体饮料 120 克", + "notes": "量未指定" + }, + { + "name": "方糖", + "quantity": null, + "unit": null, + "text_quantity": "- 方糖 9 克", + "notes": "量未指定" + }, + { + "name": "北京二锅头酒", + "quantity": null, + "unit": null, + "text_quantity": "- 北京二锅头酒 48 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "取饮用水 1177 克。" + }, + { + "step": 2, + "description": "放入酸梅晶固体饮料 60 克,使用汤匙顺时针搅拌 50 圈。" + }, + { + "step": 3, + "description": "再放入剩下 60 克酸梅晶固体饮料,再次使用汤匙顺时针搅拌 50 圈。" + }, + { + "step": 4, + "description": "放入 9 克的方糖,使用汤匙顺时针搅拌 100 圈。" + }, + { + "step": 5, + "description": "放入北京二锅头酒 48 克,用汤匙顺时针搅拌 30 圈。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-长岛冰茶", + "name": "长岛冰茶的做法", + "description": "# 长岛冰茶的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/长岛冰茶.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "金酒", + "quantity": null, + "unit": null, + "text_quantity": "- 金酒", + "notes": "量未指定" + }, + { + "name": "龙舌兰酒", + "quantity": null, + "unit": null, + "text_quantity": "- 龙舌兰酒", + "notes": "量未指定" + }, + { + "name": "伏特加", + "quantity": null, + "unit": null, + "text_quantity": "- 伏特加", + "notes": "量未指定" + }, + { + "name": "白朗姆酒", + "quantity": null, + "unit": null, + "text_quantity": "- 白朗姆酒", + "notes": "量未指定" + }, + { + "name": "橙味甜酒", + "quantity": null, + "unit": null, + "text_quantity": "- 橙味甜酒", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "枫糖浆", + "quantity": null, + "unit": null, + "text_quantity": "- 枫糖浆", + "notes": "量未指定" + }, + { + "name": "可乐", + "quantity": null, + "unit": null, + "text_quantity": "- 可乐", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "高球杯(容量", + "quantity": null, + "unit": null, + "text_quantity": "- 高球杯(容量 300ml)", + "notes": "量未指定" + }, + { + "name": "金酒", + "quantity": null, + "unit": null, + "text_quantity": "- 金酒 15ml", + "notes": "量未指定" + }, + { + "name": "龙舌兰酒", + "quantity": null, + "unit": null, + "text_quantity": "- 龙舌兰酒 15ml", + "notes": "量未指定" + }, + { + "name": "伏特加", + "quantity": null, + "unit": null, + "text_quantity": "- 伏特加 15ml", + "notes": "量未指定" + }, + { + "name": "白朗姆酒", + "quantity": null, + "unit": null, + "text_quantity": "- 白朗姆酒 15ml", + "notes": "量未指定" + }, + { + "name": "橙味甜酒", + "quantity": null, + "unit": null, + "text_quantity": "- 橙味甜酒 15ml", + "notes": "量未指定" + }, + { + "name": "柠檬汁", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬汁 30ml", + "notes": "量未指定" + }, + { + "name": "枫糖浆", + "quantity": null, + "unit": null, + "text_quantity": "- 枫糖浆 20ml", + "notes": "量未指定" + }, + { + "name": "可乐", + "quantity": null, + "unit": null, + "text_quantity": "- 可乐 75ml", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 1 个", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 100 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "柠檬对半切,挤出 30ml 柠檬汁至杯中;" + }, + { + "step": 2, + "description": "依次向杯中加入:" + }, + { + "step": 3, + "description": "向杯中缓慢倒入 20ml 枫糖浆,边倒边搅拌;" + }, + { + "step": 4, + "description": "向杯中加入 75ml 可乐;" + }, + { + "step": 5, + "description": "向杯中加入冰块直至满杯;" + }, + { + "step": 6, + "description": "轻轻搅拌 20 秒;" + }, + { + "step": 7, + "description": "开始享用." + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-冰粉-冰粉", + "name": "冰粉的做法", + "description": "# 冰粉的做法\n\n![石凉粉(冰粉)成品1](./石凉粉(冰粉)成品1.jpg)\n![石凉粉(冰粉)成品2](./石凉粉(冰粉)成品2.jpg)\n\n石凉粉,在有些地区也叫作冰粉,是河南省信阳市浉河区的一种著名特色小吃,属于豫菜系。该菜品类似果冻,但因为是天然植物做出来的,所以比果冻更健康,配上薄荷汁、柠檬汁、红豆等调料,清凉解暑。该食物深当地人的喜爱,老少皆宜。\n\n制作方法简单,只是有些耗时,预计制作时长 3 小时(其中包含 2.5 小时静置成型时间)。\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/冰粉/冰粉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/冰粉/石凉粉(冰粉)成品1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/冰粉/石凉粉(冰粉)成品1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/冰粉/石凉粉(冰粉)成品2.jpg" + ], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "冰粉籽", + "quantity": null, + "unit": null, + "text_quantity": "- 冰粉籽 200g", + "notes": "量未指定" + }, + { + "name": "过滤豆浆渣的纱布一块", + "quantity": null, + "unit": null, + "text_quantity": "- 过滤豆浆渣的纱布一块", + "notes": "量未指定" + }, + { + "name": "凉白开", + "quantity": null, + "unit": null, + "text_quantity": "- 凉白开 2000g", + "notes": "量未指定" + }, + { + "name": "薄荷汁", + "quantity": null, + "unit": null, + "text_quantity": "- 薄荷汁 10ml / 薄荷粉 10g", + "notes": "量未指定" + }, + { + "name": "一次性透明塑料杯(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 一次性透明塑料杯(可选)", + "notes": "量未指定" + }, + { + "name": "遇水发光冰块(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 遇水发光冰块(可选)", + "notes": "量未指定" + }, + { + "name": "冰粉籽", + "quantity": null, + "unit": null, + "text_quantity": "- 冰粉籽 200g", + "notes": "量未指定" + }, + { + "name": "凉白开", + "quantity": null, + "unit": null, + "text_quantity": "- 凉白开 2000g", + "notes": "量未指定" + }, + { + "name": "薄荷汁", + "quantity": null, + "unit": null, + "text_quantity": "- 薄荷汁 10ml / 薄荷粉 10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将凉白开倒入盆中;" + }, + { + "step": 2, + "description": "将冰粉籽全部用纱布包起来,开口处打结" + }, + { + "step": 3, + "description": "将包好的冰粉籽放入凉白开中,在凉白开中用力揉搓 6 分钟" + }, + { + "step": 4, + "description": "然后将凉白开放置 2.5 小时,即可成型" + }, + { + "step": 5, + "description": "随后将石凉粉用勺子装进准备好的一次性透明塑料杯中,加入 10ml 薄荷汁或者 10g 薄荷粉(柠檬汁、山楂汁、桑椹汁也可),再放入遇水发光冰块,用勺子慢慢搅拌均匀" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-奇异果菠菜特调-奇异果菠菜特调", + "name": "奇异果菠菜特调的做法", + "description": "# 奇异果菠菜特调的做法\n\n预估烹饪难度:★", + "source_path": "dishes/drink/奇异果菠菜特调/奇异果菠菜特调.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/奇异果菠菜特调/kiwi-example.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/奇异果菠菜特调/kiwi-example.jpg" + ], + "category": "饮品", + "difficulty": 1, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "原料:", + "quantity": null, + "unit": null, + "text_quantity": "- 原料:", + "notes": "量未指定" + }, + { + "name": "奇异果", + "quantity": null, + "unit": null, + "text_quantity": "- 奇异果", + "notes": "量未指定" + }, + { + "name": "苹果", + "quantity": null, + "unit": null, + "text_quantity": "- 苹果", + "notes": "量未指定" + }, + { + "name": "菠菜叶(", + "quantity": null, + "unit": null, + "text_quantity": "- 菠菜叶( 2-5 片)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "工具", + "quantity": null, + "unit": null, + "text_quantity": "- 工具", + "notes": "量未指定" + }, + { + "name": "榨汁机", + "quantity": null, + "unit": null, + "text_quantity": "- 榨汁机", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 700ml", + "notes": "量未指定" + }, + { + "name": "奇异果", + "quantity": null, + "unit": null, + "text_quantity": "- 奇异果 2 个", + "notes": "量未指定" + }, + { + "name": "苹果", + "quantity": null, + "unit": null, + "text_quantity": "- 苹果 1/2 个", + "notes": "量未指定" + }, + { + "name": "菠菜叶", + "quantity": null, + "unit": null, + "text_quantity": "- 菠菜叶 4 叶", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 12 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将猕猴桃切成两半,每半再分四份小块" + }, + { + "step": 2, + "description": "将苹果切丁" + }, + { + "step": 3, + "description": "将菠菜叶去梗,只留叶子部分" + }, + { + "step": 4, + "description": "将菠菜切碎" + }, + { + "step": 5, + "description": "一起倒入榨汁机搅拌杯" + }, + { + "step": 6, + "description": "注水" + }, + { + "step": 7, + "description": "加入白砂糖" + }, + { + "step": 8, + "description": "启动搅拌机,搅拌约 4 个 15 秒(每 15 秒停下看状态)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-柠檬水-柠檬水", + "name": "柠檬水的做法", + "description": "# 柠檬水的做法\n\n![柠檬水成品](./柠檬水.jpg)\n\n预估烹饪难度:★", + "source_path": "dishes/drink/柠檬水/柠檬水.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/柠檬水/柠檬水.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/柠檬水/柠檬水.jpg" + ], + "category": "饮品", + "difficulty": 1, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "原料", + "quantity": null, + "unit": null, + "text_quantity": "- 原料", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "果蜜", + "quantity": null, + "unit": null, + "text_quantity": "- 果蜜", + "notes": "量未指定" + }, + { + "name": "冰(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 冰(可选)", + "notes": "量未指定" + }, + { + "name": "工具", + "quantity": null, + "unit": null, + "text_quantity": "- 工具", + "notes": "量未指定" + }, + { + "name": "雪克杯", + "quantity": null, + "unit": null, + "text_quantity": "- 雪克杯", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 40~45 克", + "notes": "量未指定" + }, + { + "name": "果蜜", + "quantity": null, + "unit": null, + "text_quantity": "- 果蜜 40~45 克", + "notes": "量未指定" + }, + { + "name": "冰几块(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 冰几块(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "称 40~45 克柠檬,放入雪克杯中" + }, + { + "step": 2, + "description": "雪克杯盖盖子锤大约 10 次" + }, + { + "step": 3, + "description": "加入果蜜 40~45 克" + }, + { + "step": 4, + "description": "补水" + }, + { + "step": 5, + "description": "摇晃均匀" + }, + { + "step": 6, + "description": "最后根据喜好加冰" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-泰国手标红茶-泰国手标红茶", + "name": "泰国手标红茶的做法", + "description": "# 泰国手标红茶的做法\n\n![泰国手标红茶成品](./泰国手标红茶.jpg)\n\n泰国手标红茶是泰国街头随处可见的奶茶,味道香纯,绵密。\n\n预估烹饪难度:★★★", + "source_path": "dishes/drink/泰国手标红茶/泰国手标红茶.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/泰国手标红茶/泰国手标红茶.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/泰国手标红茶/泰国手标红茶.jpg" + ], + "category": "饮品", + "difficulty": 3, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "茶粉", + "quantity": null, + "unit": null, + "text_quantity": "- 茶粉", + "notes": "量未指定" + }, + { + "name": "炼乳", + "quantity": null, + "unit": null, + "text_quantity": "- 炼乳", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "克称", + "quantity": null, + "unit": null, + "text_quantity": "- 克称", + "notes": "量未指定" + }, + { + "name": "带刻度容器", + "quantity": null, + "unit": null, + "text_quantity": "- 带刻度容器", + "notes": "量未指定" + }, + { + "name": "港式奶茶过滤袋", + "quantity": null, + "unit": null, + "text_quantity": "- 港式奶茶过滤袋", + "notes": "量未指定" + }, + { + "name": "水(600cc)", + "quantity": null, + "unit": null, + "text_quantity": "- 水(600cc)", + "notes": "量未指定" + }, + { + "name": "茶粉(20g)", + "quantity": null, + "unit": null, + "text_quantity": "- 茶粉(20g)", + "notes": "量未指定" + }, + { + "name": "白糖(24g)", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖(24g)", + "notes": "量未指定" + }, + { + "name": "牛奶(18ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶(18ml)", + "notes": "量未指定" + }, + { + "name": "炼乳(24g)", + "quantity": null, + "unit": null, + "text_quantity": "- 炼乳(24g)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "600cc 水大火烧开" + }, + { + "step": 2, + "description": "在过滤袋中装入 20g 茶粉,开水倒入过滤袋中,过滤 20 遍" + }, + { + "step": 3, + "description": "使用克称量取 24g 炼乳、24g 白糖和 18ml 牛奶放入 1000ml 以上的水壶中" + }, + { + "step": 4, + "description": "将过滤好的茶水倒入水壶中搅拌,直到白糖融化" + }, + { + "step": 5, + "description": "将水壶放到冰箱 4 小时以上" + }, + { + "step": 6, + "description": "喝前可以加 6-8 颗冰块" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-海边落日-海边落日", + "name": "海边落日的做法", + "description": "# 海边落日的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★★", + "source_path": "dishes/drink/海边落日/海边落日.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/海边落日/海边落日.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/海边落日/海边落日.jpg" + ], + "category": "饮品", + "difficulty": 3, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "红石榴糖浆", + "quantity": null, + "unit": null, + "text_quantity": "- 红石榴糖浆", + "notes": "量未指定" + }, + { + "name": "NFC 橙汁", + "quantity": null, + "unit": null, + "text_quantity": "- NFC 橙汁", + "notes": "量未指定" + }, + { + "name": "苏打水", + "quantity": null, + "unit": null, + "text_quantity": "- 苏打水", + "notes": "量未指定" + }, + { + "name": "白朗姆", + "quantity": null, + "unit": null, + "text_quantity": "- 白朗姆", + "notes": "量未指定" + }, + { + "name": "蓝橙力娇酒", + "quantity": null, + "unit": null, + "text_quantity": "- 蓝橙力娇酒", + "notes": "量未指定" + }, + { + "name": "柠檬汁", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬汁", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "大号的玻璃杯", + "quantity": null, + "unit": null, + "text_quantity": "- 大号的玻璃杯", + "notes": "量未指定" + }, + { + "name": "搅拌棒", + "quantity": null, + "unit": null, + "text_quantity": "- 搅拌棒", + "notes": "量未指定" + }, + { + "name": "量酒器", + "quantity": null, + "unit": null, + "text_quantity": "- 量酒器", + "notes": "量未指定" + }, + { + "name": "调酒杯", + "quantity": null, + "unit": null, + "text_quantity": "- 调酒杯", + "notes": "量未指定" + }, + { + "name": "吸管", + "quantity": null, + "unit": null, + "text_quantity": "- 吸管", + "notes": "量未指定" + }, + { + "name": "水果刀", + "quantity": null, + "unit": null, + "text_quantity": "- 水果刀", + "notes": "量未指定" + }, + { + "name": "红石榴糖浆", + "quantity": null, + "unit": null, + "text_quantity": "- 红石榴糖浆 15ml", + "notes": "量未指定" + }, + { + "name": "橙汁", + "quantity": null, + "unit": null, + "text_quantity": "- 橙汁 35~50ml", + "notes": "量未指定" + }, + { + "name": "苏打水", + "quantity": null, + "unit": null, + "text_quantity": "- 苏打水 50ml", + "notes": "量未指定" + }, + { + "name": "白朗姆", + "quantity": null, + "unit": null, + "text_quantity": "- 白朗姆 30ml", + "notes": "量未指定" + }, + { + "name": "蓝橙力娇酒", + "quantity": null, + "unit": null, + "text_quantity": "- 蓝橙力娇酒 15ml", + "notes": "量未指定" + }, + { + "name": "柠檬汁", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬汁 15ml", + "notes": "量未指定" + }, + { + "name": "大冰块差不多就行", + "quantity": null, + "unit": null, + "text_quantity": "- 大冰块差不多就行", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 1 片", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-百香果橙子特调-百香果橙子特调", + "name": "百香果橙子特调的做法", + "description": "# 百香果橙子特调的做法\n\n茉莉绿茶版本\n\n![tea](./tea-version.jpg)\n\n苏打气泡水版本\n\n![tea](./soda-version.jpg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/drink/百香果橙子特调/百香果橙子特调.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/百香果橙子特调/soda-version.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/百香果橙子特调/soda-version.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/百香果橙子特调/tea-version.jpg" + ], + "category": "饮品", + "difficulty": 3, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "原料:", + "quantity": null, + "unit": null, + "text_quantity": "- 原料:", + "notes": "量未指定" + }, + { + "name": "百香果", + "quantity": null, + "unit": null, + "text_quantity": "- 百香果", + "notes": "量未指定" + }, + { + "name": "橙子", + "quantity": null, + "unit": null, + "text_quantity": "- 橙子", + "notes": "量未指定" + }, + { + "name": "茉莉绿茶茶叶/苏打气泡水二选一", + "quantity": null, + "unit": null, + "text_quantity": "- 茉莉绿茶茶叶/苏打气泡水二选一", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "蜂蜜(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 蜂蜜(可选)", + "notes": "量未指定" + }, + { + "name": "薄荷叶或其他绿叶(可选,装饰使用)", + "quantity": null, + "unit": null, + "text_quantity": "- 薄荷叶或其他绿叶(可选,装饰使用)", + "notes": "量未指定" + }, + { + "name": "工具", + "quantity": null, + "unit": null, + "text_quantity": "- 工具", + "notes": "量未指定" + }, + { + "name": "手动压汁器", + "quantity": null, + "unit": null, + "text_quantity": "- 手动压汁器", + "notes": "量未指定" + }, + { + "name": "基于茉莉绿茶版本准备,一杯分量,约", + "quantity": null, + "unit": null, + "text_quantity": "- 基于茉莉绿茶版本准备,一杯分量,约 380 毫升", + "notes": "量未指定" + }, + { + "name": "橙子", + "quantity": null, + "unit": null, + "text_quantity": "- 橙子 1 个(约 200 克,拳头大小)", + "notes": "量未指定" + }, + { + "name": "茉莉绿茶茶叶", + "quantity": null, + "unit": null, + "text_quantity": "- 茉莉绿茶茶叶 3~6 克", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 150 毫升", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 160 克以上", + "notes": "量未指定" + }, + { + "name": "腌制百香果部分(因为量小不好配置,这里是两次的分量)", + "quantity": null, + "unit": null, + "text_quantity": "- 腌制百香果部分(因为量小不好配置,这里是两次的分量)", + "notes": "量未指定" + }, + { + "name": "百香果", + "quantity": null, + "unit": null, + "text_quantity": "- 百香果 3 个", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 30 克", + "notes": "量未指定" + }, + { + "name": "蜂蜜", + "quantity": null, + "unit": null, + "text_quantity": "- 蜂蜜 10 克(如果没有可以用 5 克白砂糖代替)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "百香果腌制(因为量小不好配置,这里是两次的分量)" + }, + { + "step": 2, + "description": "茉莉绿茶调配(推荐比例=>茶 : 水 : 冰 = 1~2 : 50 : 30)" + }, + { + "step": 3, + "description": "橙子的处理(可在泡茶期间处理)" + }, + { + "step": 4, + "description": "正式调配" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-砂糖椰子冰沙-砂糖椰子冰沙", + "name": "砂糖椰子冰沙的做法", + "description": "# 砂糖椰子冰沙的做法\n\n![alt text](砂糖椰子冰沙-1.jpg)\n\n砂糖椰子冰沙是一种制作极其快速方便的饮料,若原料选择得当则口感丰富。然而制作时动静较大,适合白天在家制作以作为下午茶。\n\n预估烹饪难度:★", + "source_path": "dishes/drink/砂糖椰子冰沙/砂糖椰子冰沙.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/砂糖椰子冰沙/砂糖椰子冰沙-1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/砂糖椰子冰沙/砂糖椰子冰沙-1.jpg" + ], + "category": "饮品", + "difficulty": 1, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "瓶装椰汁(瓶口较大为佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 瓶装椰汁(瓶口较大为佳)", + "notes": "量未指定" + }, + { + "name": "咖啡调糖(黄色粗粒)", + "quantity": null, + "unit": null, + "text_quantity": "- 咖啡调糖(黄色粗粒)", + "notes": "量未指定" + }, + { + "name": "瓶装椰子汁", + "quantity": null, + "unit": null, + "text_quantity": "- 瓶装椰子汁 500ml", + "notes": "量未指定" + }, + { + "name": "咖啡调糖", + "quantity": null, + "unit": null, + "text_quantity": "- 咖啡调糖 10g(两包太古咖啡调糖)", + "notes": "量未指定" + }, + { + "name": "坚果碎(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 坚果碎(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "500ml 瓶装椰汁倒掉 200ml,立刻拧紧瓶盖。" + }, + { + "step": 2, + "description": "将这瓶椰汁放入冰箱冷冻区并冷冻 10 小时以上。" + }, + { + "step": 3, + "description": "将这瓶椰汁取出,若确认瓶中椰汁已彻底冻结,则在墙角、椅背、桌角等坚硬表面上用力抽打。(请务必确认表面不会因此受到损伤)" + }, + { + "step": 4, + "description": "当抽打到冻结椰汁变成冰沙状态,打开瓶盖倒出冰沙。" + }, + { + "step": 5, + "description": "在冰沙表面均匀撒上咖啡调糖或坚果碎。" + }, + { + "step": 6, + "description": "完成" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-耙耙柑茶-耙耙柑茶", + "name": "耙耙柑茶的做法", + "description": "# 耙耙柑茶的做法\n\n![citrus-tea](citrus-tea.jpg)\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/耙耙柑茶/耙耙柑茶.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/耙耙柑茶/citrus-tea.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/耙耙柑茶/citrus-tea.jpg" + ], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "原料:", + "quantity": null, + "unit": null, + "text_quantity": "- 原料:", + "notes": "量未指定" + }, + { + "name": "耙耙柑(替换物请看附加内容)", + "quantity": null, + "unit": null, + "text_quantity": "- 耙耙柑(替换物请看附加内容)", + "notes": "量未指定" + }, + { + "name": "茉莉绿茶", + "quantity": null, + "unit": null, + "text_quantity": "- 茉莉绿茶", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "[蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- [蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)(可选)", + "notes": "量未指定" + }, + { + "name": "工具", + "quantity": null, + "unit": null, + "text_quantity": "- 工具", + "notes": "量未指定" + }, + { + "name": "搅拌机", + "quantity": null, + "unit": null, + "text_quantity": "- 搅拌机", + "notes": "量未指定" + }, + { + "name": "耙耙柑", + "quantity": null, + "unit": null, + "text_quantity": "- 耙耙柑 1~2 个(200 克以上)", + "notes": "量未指定" + }, + { + "name": "茉莉绿茶", + "quantity": null, + "unit": null, + "text_quantity": "- 茉莉绿茶 2~4 克", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 60 克", + "notes": "量未指定" + }, + { + "name": "1 :", + "quantity": null, + "unit": null, + "text_quantity": "- 1 : 1 蔗糖糖浆 10 克(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "茉莉绿茶调配(推荐比例=>茶 : 水 : 冰 = 1~2 : 50 : 30)" + }, + { + "step": 2, + "description": "正式调配" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-菠萝咖啡特调-菠萝咖啡特调", + "name": "菠萝咖啡特调的做法", + "description": "# 菠萝咖啡特调的做法\n\n![示例菜成品](./菠萝咖啡特调.png)\n\n菠萝咖啡特调是非常适合家庭出品的饮料,酸甜可口。\n\n预估烹饪难度:★★★", + "source_path": "dishes/drink/菠萝咖啡特调/菠萝咖啡特调.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/菠萝咖啡特调/菠萝咖啡特调.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/菠萝咖啡特调/菠萝咖啡特调.png" + ], + "category": "饮品", + "difficulty": 3, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "咖啡液(推荐浓缩或者冷萃)", + "quantity": null, + "unit": null, + "text_quantity": "- 咖啡液(推荐浓缩或者冷萃)", + "notes": "量未指定" + }, + { + "name": "菠萝汁(鲜榨或者 nfc)", + "quantity": null, + "unit": null, + "text_quantity": "- 菠萝汁(鲜榨或者 nfc)", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "苏打水", + "quantity": null, + "unit": null, + "text_quantity": "- 苏打水", + "notes": "量未指定" + }, + { + "name": "奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 奶油", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "海盐(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 海盐(可选)", + "notes": "量未指定" + }, + { + "name": "朗姆酒 (可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 朗姆酒 (可选)", + "notes": "量未指定" + }, + { + "name": "咖啡液", + "quantity": null, + "unit": null, + "text_quantity": "- 咖啡液 30ml", + "notes": "量未指定" + }, + { + "name": "菠萝汁", + "quantity": null, + "unit": null, + "text_quantity": "- 菠萝汁 60ml", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 50g", + "notes": "量未指定" + }, + { + "name": "苏打水", + "quantity": null, + "unit": null, + "text_quantity": "- 苏打水 30ml", + "notes": "量未指定" + }, + { + "name": "奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 奶油 30ml", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 10ml", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 8g", + "notes": "量未指定" + }, + { + "name": "海盐", + "quantity": null, + "unit": null, + "text_quantity": "- 海盐 0.5g", + "notes": "量未指定" + }, + { + "name": "朗姆酒", + "quantity": null, + "unit": null, + "text_quantity": "- 朗姆酒 5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "杯子里依次加入冰块,咖啡液,菠萝汁和苏打水" + }, + { + "step": 2, + "description": "奶油加糖打发至湿性发泡,加入朗姆酒和牛奶均匀只有流动性" + }, + { + "step": 3, + "description": "在第一部混合液上方倒入奶油" + }, + { + "step": 4, + "description": "奶油顶面撒上海盐" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-酒酿醪糟-酒酿醪糟", + "name": "酒酿醪糟的做法", + "description": "# 酒酿(醪糟)的做法\n\n![酒酿成品](./酒酿醪糟.jpeg)\n\n酒酿,也叫醪糟,是一道传统中式发酵甜品。成品清甜微醺,含少量酒精,具有健脾开胃、促进消化的功效。虽然制作需要一定发酵技巧,但过程简单有趣,是发酵入门好选择。预计制作时间为 2 天(不含等待时间操作仅需 1 小时左右)。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/drink/酒酿醪糟/酒酿醪糟.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/酒酿醪糟/酒酿米糕.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/酒酿醪糟/酒酿米糕.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/酒酿醪糟/酒酿醪糟.jpeg" + ], + "category": "饮品", + "difficulty": 4, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "糯米", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米 800g(推荐使用圆糯米)", + "notes": "量未指定" + }, + { + "name": "安琪甜酒曲一包 (8g)(虽然按比例为每", + "quantity": null, + "unit": null, + "text_quantity": "- 安琪甜酒曲一包 (8g)(虽然按比例为每 1000g 糯米用 3g,但多放酒曲能提高成功概率)", + "notes": "量未指定" + }, + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水 720g + 600g(720 克用于蒸饭,后 500g 用于发酵)", + "notes": "量未指定" + }, + { + "name": "蒸锅(电饭煲即可)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸锅(电饭煲即可)", + "notes": "量未指定" + }, + { + "name": "温度计(可选但推荐)", + "quantity": null, + "unit": null, + "text_quantity": "- 温度计(可选但推荐)", + "notes": "量未指定" + }, + { + "name": "干净密封玻璃或陶瓷容器", + "quantity": null, + "unit": null, + "text_quantity": "- 干净密封玻璃或陶瓷容器 1 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 800g 糯米淘洗干净放入电饭煲,加入 720g 清水选择蒸饭模式" + }, + { + "step": 2, + "description": "蒸熟后将米饭倒出摊凉,使用干净的工具将其摊至 30°C 左右(用温度计测量为宜,体感温热但不烫手)" + }, + { + "step": 3, + "description": "将 8g 安琪甜酒曲用 20ml 温水(约 30°C)化开,均匀撒在糯米饭中,同时翻拌均匀" + }, + { + "step": 4, + "description": "向糯米饭中加入 600g 清水帮助酒曲翻拌均匀。静置 2-3 分钟后发现糯米饭吸饱水分。这次加水可以让酒酿首次发酵便汤汁丰富" + }, + { + "step": 5, + "description": "用擀面杖在糯米饭中央挖一个小洞(便于出酒)" + }, + { + "step": 6, + "description": "将混合好的米饭装入干净容器中,轻轻压平表面,盖上盖子或保鲜膜密封好" + }, + { + "step": 7, + "description": "放置于 28 ~ 32°C 环境下发酵 24 ~ 48 小时。发酵期间不可剧烈摇晃或移动" + }, + { + "step": 8, + "description": "发酵成功标准为:中间凹槽有透明酒液渗出,整体略带酒香,无异味、不酸败" + }, + { + "step": 9, + "description": "发酵结束后可立即冷藏保存(过程中可以加入桂花),每次食用用干净工具取出,可冷藏保存 7 ~ 10 天" + }, + { + "step": 10, + "description": "可以继续二次发酵,加入适量清水增加酒酿产量(800g 水以内即可)" + }, + { + "step": 11, + "description": "酒酿会一直持续发酵。如果想停止发酵,可以上锅蒸 10 分钟" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-酸梅汤-酸梅汤", + "name": "酸梅汤的做法", + "description": "# 酸梅汤的做法\n\n![酸梅汤](./imges/sour_plum_soup.jpg)\n\n视频演示: [链接](https://www.bilibili.com/video/BV1164y1F7hv/)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/drink/酸梅汤/酸梅汤.md", + "image_path": null, + "images": [], + "category": "饮品", + "difficulty": 4, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "乌枣", + "quantity": null, + "unit": null, + "text_quantity": "- 乌枣", + "notes": "量未指定" + }, + { + "name": "乌梅", + "quantity": null, + "unit": null, + "text_quantity": "- 乌梅", + "notes": "量未指定" + }, + { + "name": "山楂片(生)", + "quantity": null, + "unit": null, + "text_quantity": "- 山楂片(生)", + "notes": "量未指定" + }, + { + "name": "黄冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 黄冰糖", + "notes": "量未指定" + }, + { + "name": "甘草", + "quantity": null, + "unit": null, + "text_quantity": "- 甘草", + "notes": "量未指定" + }, + { + "name": "陈皮", + "quantity": null, + "unit": null, + "text_quantity": "- 陈皮", + "notes": "量未指定" + }, + { + "name": "红豆蔻", + "quantity": null, + "unit": null, + "text_quantity": "- 红豆蔻", + "notes": "量未指定" + }, + { + "name": "干桂花", + "quantity": null, + "unit": null, + "text_quantity": "- 干桂花", + "notes": "量未指定" + }, + { + "name": "两升水", + "quantity": null, + "unit": null, + "text_quantity": "- 两升水", + "notes": "量未指定" + }, + { + "name": "乌枣", + "quantity": null, + "unit": null, + "text_quantity": "- 乌枣 25 克", + "notes": "量未指定" + }, + { + "name": "乌梅", + "quantity": null, + "unit": null, + "text_quantity": "- 乌梅 25g", + "notes": "量未指定" + }, + { + "name": "黄冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 黄冰糖 100 克", + "notes": "量未指定" + }, + { + "name": "山楂片", + "quantity": null, + "unit": null, + "text_quantity": "- 山楂片 30 克", + "notes": "量未指定" + }, + { + "name": "甘草", + "quantity": null, + "unit": null, + "text_quantity": "- 甘草 2 克", + "notes": "量未指定" + }, + { + "name": "陈皮", + "quantity": null, + "unit": null, + "text_quantity": "- 陈皮 4 克", + "notes": "量未指定" + }, + { + "name": "红豆蔻", + "quantity": null, + "unit": null, + "text_quantity": "- 红豆蔻 1 克", + "notes": "量未指定" + }, + { + "name": "干桂花", + "quantity": null, + "unit": null, + "text_quantity": "- 干桂花 3 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "冲洗材料(干桂花和冰糖除外), 1.5 升水常温浸泡两小时以上(干桂花和冰糖除外)" + }, + { + "step": 2, + "description": "开中大火煮沸,盖盖,转小火煮 40 分钟,为头煎" + }, + { + "step": 3, + "description": "将冰糖放入盆内,再将沥好用材的头汤趁热倒入,搅拌至冰糖融化。" + }, + { + "step": 4, + "description": "药材重新装回锅内再 600 毫升的水,开大火煮沸,盖盖,转中火,再煮 20 分钟为二煎" + }, + { + "step": 5, + "description": "最后将二煎和冰糖水趁热混合为成品。在成品 60-70℃加入干桂花(不要超过 80℃)加盖晾凉再放入冰箱冷藏 3 小时以上。" + }, + { + "step": 6, + "description": "饮用时记得将干桂花沥出。如饮茶般细啜,冰凉振齿,酸醒人、甜适度,滋味丰满而悠长" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-金汤力-金汤力", + "name": "金汤力的做法", + "description": "# 金汤力的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/金汤力/金汤力.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金汤力/gin-tonic.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金汤力/gin-tonic.jpg" + ], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "金酒", + "quantity": null, + "unit": null, + "text_quantity": "- 金酒", + "notes": "量未指定" + }, + { + "name": "汤力水气泡水", + "quantity": null, + "unit": null, + "text_quantity": "- 汤力水气泡水", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "新鲜绿叶(可选,装饰用)", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜绿叶(可选,装饰用)", + "notes": "量未指定" + }, + { + "name": "手动压汁器", + "quantity": null, + "unit": null, + "text_quantity": "- 手动压汁器", + "notes": "量未指定" + }, + { + "name": "金酒", + "quantity": null, + "unit": null, + "text_quantity": "- 金酒 30~40 毫升", + "notes": "量未指定" + }, + { + "name": "汤力水气泡水", + "quantity": null, + "unit": null, + "text_quantity": "- 汤力水气泡水 1 罐", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 1 个", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 100 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-drink-金菲士-金菲士", + "name": "金菲士的做法", + "description": "# 金菲士的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★", + "source_path": "dishes/drink/金菲士/金菲士.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金菲士/gin-fizz.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金菲士/gin-fizz.jpg" + ], + "category": "饮品", + "difficulty": 2, + "tags": [ + "饮品" + ], + "servings": 1, + "ingredients": [ + { + "name": "金酒", + "quantity": null, + "unit": null, + "text_quantity": "- 金酒", + "notes": "量未指定" + }, + { + "name": "苏打气泡水", + "quantity": null, + "unit": null, + "text_quantity": "- 苏打气泡水", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "[蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)", + "quantity": null, + "unit": null, + "text_quantity": "- [蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块", + "notes": "量未指定" + }, + { + "name": "新鲜绿叶(可选,装饰用)", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜绿叶(可选,装饰用)", + "notes": "量未指定" + }, + { + "name": "手动压汁器", + "quantity": null, + "unit": null, + "text_quantity": "- 手动压汁器", + "notes": "量未指定" + }, + { + "name": "雪克瓶(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 雪克瓶(可选)", + "notes": "量未指定" + }, + { + "name": "金酒", + "quantity": null, + "unit": null, + "text_quantity": "- 金酒 30~40 毫升", + "notes": "量未指定" + }, + { + "name": "苏打气泡水", + "quantity": null, + "unit": null, + "text_quantity": "- 苏打气泡水 1 罐", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 1 个", + "notes": "量未指定" + }, + { + "name": "1 :", + "quantity": null, + "unit": null, + "text_quantity": "- 1 : 1 蔗糖糖浆 30~40 克", + "notes": "量未指定" + }, + { + "name": "冰块", + "quantity": null, + "unit": null, + "text_quantity": "- 冰块 100 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-乡村啤酒鸭", + "name": "乡村啤酒鸭的做法", + "description": "# 乡村啤酒鸭的做法\n\n![乡村啤酒鸭](https://jphuang-image.oss-cn-beijing.aliyuncs.com/beer/duck/%E6%88%90%E5%93%812.jpg)\n\n将鸭肉与啤酒一同炖煮成菜,使滋补的鸭肉味道更加浓厚,鸭肉不仅入口鲜香,还带有一股啤酒清香。一般初学者只需要 1 小时即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/乡村啤酒鸭.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸭肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鸭肉", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒", + "notes": "量未指定" + }, + { + "name": "32 厘米以上的炒锅一个(锅太小难炒", + "quantity": null, + "unit": null, + "text_quantity": "- 32 厘米以上的炒锅一个(锅太小难炒", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "红椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红椒", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "草果", + "quantity": null, + "unit": null, + "text_quantity": "- 草果", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "鸭肉(半只,1 kg,让市场老板剁成小块)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸭肉(半只,1 kg,让市场老板剁成小块)", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒 1000 ml (可以买 500 ml 的罐装啤酒两瓶)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸭肉清洗一遍放进锅中" + }, + { + "step": 2, + "description": "加清水淹没鸭肉" + }, + { + "step": 3, + "description": "加 20 ml 料酒" + }, + { + "step": 4, + "description": "加备用的 1 根 大葱" + }, + { + "step": 5, + "description": "加生姜 ,拍散的 2 厘米" + }, + { + "step": 6, + "description": "开火烧滚" + }, + { + "step": 7, + "description": "捞出浮沫" + }, + { + "step": 8, + "description": "鸭肉捞出,清水洗干净备用" + }, + { + "step": 9, + "description": "锅清洗感觉烧热,加 60ml 的花生油" + }, + { + "step": 10, + "description": "油温到 60 度的时候,加一把花椒( 30 颗)" + }, + { + "step": 11, + "description": "加鸭肉翻炒 4 分钟" + }, + { + "step": 12, + "description": "加入 1000 ml 的啤酒" + }, + { + "step": 13, + "description": "烧鸭肉 30 分钟" + }, + { + "step": 14, + "description": "出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-冷吃兔", + "name": "冷吃兔的做法", + "description": "# 冷吃兔的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/冷吃兔.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "兔肉", + "quantity": null, + "unit": null, + "text_quantity": "- 兔肉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "小葱/大葱/洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱/大葱/洋葱", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "青花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "山奈", + "quantity": null, + "unit": null, + "text_quantity": "- 山奈", + "notes": "量未指定" + }, + { + "name": "白蔻", + "quantity": null, + "unit": null, + "text_quantity": "- 白蔻", + "notes": "量未指定" + }, + { + "name": "小茴香", + "quantity": null, + "unit": null, + "text_quantity": "- 小茴香", + "notes": "量未指定" + }, + { + "name": "白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻", + "notes": "量未指定" + }, + { + "name": "盐量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 兔肉斤数 * 2 克", + "notes": "量未指定" + }, + { + "name": "味精量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 味精量 = 兔肉斤数 * 1 克", + "notes": "量未指定" + }, + { + "name": "蚝油量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油量 = 兔肉斤数 * 5 克", + "notes": "量未指定" + }, + { + "name": "料酒量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒量 = 兔肉斤数 * 10 克", + "notes": "量未指定" + }, + { + "name": "油量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 油量 = 兔肉斤数 * 0.9 ~ 1 升", + "notes": "量未指定" + }, + { + "name": "蒜量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜量 = 兔肉斤数 * 二分之一头蒜", + "notes": "量未指定" + }, + { + "name": "姜量 = 蒜量", + "quantity": null, + "unit": null, + "text_quantity": "- 姜量 = 蒜量", + "notes": "量未指定" + }, + { + "name": "小葱/大葱/洋葱总量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱/大葱/洋葱总量 = 兔肉斤数 * 15 克", + "notes": "量未指定" + }, + { + "name": "干辣椒量 = 辣椒段的总体积等于兔肉的总体积", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒量 = 辣椒段的总体积等于兔肉的总体积", + "notes": "量未指定" + }, + { + "name": "青花椒量 =", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒量 = 3 斤兔肉对应吃饭用的小碗,一整碗花椒", + "notes": "量未指定" + }, + { + "name": "八角量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 八角量 = 兔肉斤数 * 1 粒", + "notes": "量未指定" + }, + { + "name": "桂皮量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮量 = 兔肉斤数 * 大拇指长短的一块", + "notes": "量未指定" + }, + { + "name": "香叶量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶量 = 兔肉斤数 * 5 片", + "notes": "量未指定" + }, + { + "name": "山奈量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 山奈量 = 兔肉斤数 * 黄豆大小的一块", + "notes": "量未指定" + }, + { + "name": "白蔻量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 白蔻量 = 兔肉斤数 * 2 颗", + "notes": "量未指定" + }, + { + "name": "小茴香量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 小茴香量 = 兔肉斤数 * 15 克", + "notes": "量未指定" + }, + { + "name": "白芝麻量 = 兔肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻量 = 兔肉斤数 * 25 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-可乐鸡翅", + "name": "可乐鸡翅的做法", + "description": "# 可乐鸡翅的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/可乐鸡翅.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡翅中", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡翅中", + "notes": "量未指定" + }, + { + "name": "可乐", + "quantity": null, + "unit": null, + "text_quantity": "- 可乐", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "料酒或啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒或啤酒", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "鸡翅", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡翅 10 ~ 12 只", + "notes": "量未指定" + }, + { + "name": "可乐", + "quantity": null, + "unit": null, + "text_quantity": "- 可乐 500ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 10 克", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15 克", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 3 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 克", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 2 片", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 20 毫升", + "notes": "量未指定" + }, + { + "name": "小葱挽成结", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱挽成结", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡翅入锅,倒入冷水淹没。放生姜 1 片和料酒 10 ~ 20 毫升。大火煮开( 大约 2 分钟 )后,撇去浮沫,沥出水分" + }, + { + "step": 2, + "description": "捞出鸡翅,可用刀将两边各划上两口改刀。生抽约 10 克腌制鸡翅 10 分钟(生抽能完全包裹鸡翅表面入味就行)" + }, + { + "step": 3, + "description": "锅重新小火起油,先将剩余姜片爆香,然后下入腌好的鸡翅。将鸡翅煎至金黄翻面(直到两面金黄),用炒菜勺子翻动一下鸡翅,与姜片一起翻炒 4~5 下(目的是防止鸡翅和姜片粘黏)。" + }, + { + "step": 4, + "description": "鸡翅金黄,倒入可乐没过鸡翅,开大火将锅中可乐煮沸,然后撇去漂浮的黑色浮沫(包含血水)。此时加入葱结。" + }, + { + "step": 5, + "description": "调味:加入食用盐 2 克,白糖 10 克,生抽 3 克调味(可以适当用老抽调底色,3 克)。" + }, + { + "step": 6, + "description": "等到葱结变黄,和姜片一起捞出,转中火继续慢煮可乐鸡翅。" + }, + { + "step": 7, + "description": "等到可乐呈现挂丝状态,关小火让汁牢牢挂在鸡翅上。出锅,装盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-咕噜肉", + "name": "咕噜肉的做法", + "description": "# 咕噜肉的做法\n\n咕噜肉是非常下饭的菜肴,只需一道就可以吃得津津有味,大人小孩都爱吃。而这次做的是简易版菠萝咕噜肉,利用简单的材料就可以在家做出特有风味的咕噜肉 。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/咕噜肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "梅头猪肉", + "quantity": null, + "unit": null, + "text_quantity": "- 梅头猪肉", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "罐头菠萝片", + "quantity": null, + "unit": null, + "text_quantity": "- 罐头菠萝片", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "茄汁", + "quantity": null, + "unit": null, + "text_quantity": "- 茄汁", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋", + "notes": "量未指定" + }, + { + "name": "蒜蓉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜蓉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "1 汤匙 =", + "quantity": null, + "unit": null, + "text_quantity": "- 1 汤匙 = 15ml", + "notes": "量未指定" + }, + { + "name": "1 茶匙 =", + "quantity": null, + "unit": null, + "text_quantity": "- 1 茶匙 = 5ml", + "notes": "量未指定" + }, + { + "name": "梅头猪肉", + "quantity": null, + "unit": null, + "text_quantity": "- 梅头猪肉 100g", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 25g", + "notes": "量未指定" + }, + { + "name": "罐头菠萝片", + "quantity": null, + "unit": null, + "text_quantity": "- 罐头菠萝片 75g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1/4 茶匙", + "notes": "量未指定" + }, + { + "name": "茄汁", + "quantity": null, + "unit": null, + "text_quantity": "- 茄汁 4 汤匙", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋 2 茶匙", + "notes": "量未指定" + }, + { + "name": "蒜蓉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜蓉 1 汤匙", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 1/2 茶匙", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉 2 1/2 茶匙", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 2 汤匙", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 200 毫升", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将梅头猪肉(100 克)洗净,然后用厨房纸抹干水份,将猪肉切成比想要的成品小一圈的大小。" + }, + { + "step": 2, + "description": "用盐(1/2 茶匙)腌制梅头猪肉 20 分钟。" + }, + { + "step": 3, + "description": "将青椒(25 克)切碎。" + }, + { + "step": 4, + "description": "将菠萝片(75 克)切件。" + }, + { + "step": 5, + "description": "在碗中加入茄汁(4 汤匙)﹑白醋(2 茶匙)﹑蒜蓉(1 汤匙)﹑生抽(½ 茶匙)﹑生粉(2½ 茶匙)﹑白砂糖(2 汤匙)﹑盐(¼ 茶匙)和水(200 毫升),拌匀成酱汁。" + }, + { + "step": 6, + "description": "将梅头猪肉粒沾上生粉(6 汤匙)。" + }, + { + "step": 7, + "description": "加入油(500 毫升)中火加热。" + }, + { + "step": 8, + "description": "将梅头猪肉粒放至锅里中火炸 5 分钟,然后盛起。" + }, + { + "step": 9, + "description": "加入梅头猪肉粒,再大火翻炸 1 分钟。" + }, + { + "step": 10, + "description": "加入油(1 茶匙)和酱汁,中火加热 3 分钟。" + }, + { + "step": 11, + "description": "加入青椒和菠萝,大火加热 2 分钟。" + }, + { + "step": 12, + "description": "将已炸好的梅头猪肉粒与酱汁拌匀即可。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-商芝肉", + "name": "商芝肉的做法", + "description": "# 商芝肉的做法\n\n此菜色泽红润,质地软嫩,肥而不腻,有浓郁的商芝香味,是陕西省商县特有的风味菜。因商芝属于陕西特产,此菜原料获取难度较大,不易制作。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/商芝肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "带皮猪五花肉(去骨)", + "quantity": null, + "unit": null, + "text_quantity": "- 带皮猪五花肉(去骨)", + "notes": "量未指定" + }, + { + "name": "商芝(又名紫萁,属蕨类,嫩叶可食)", + "quantity": null, + "unit": null, + "text_quantity": "- 商芝(又名紫萁,属蕨类,嫩叶可食)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "蜂蜜", + "quantity": null, + "unit": null, + "text_quantity": "- 蜂蜜", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "摊鸡蛋皮", + "quantity": null, + "unit": null, + "text_quantity": "- 摊鸡蛋皮", + "notes": "量未指定" + }, + { + "name": "精盐", + "quantity": null, + "unit": null, + "text_quantity": "- 精盐", + "notes": "量未指定" + }, + { + "name": "鸡汤", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡汤", + "notes": "量未指定" + }, + { + "name": "芝麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油", + "notes": "量未指定" + }, + { + "name": "熟猪油", + "quantity": null, + "unit": null, + "text_quantity": "- 熟猪油", + "notes": "量未指定" + }, + { + "name": "带皮猪五花肉:", + "quantity": null, + "unit": null, + "text_quantity": "- 带皮猪五花肉: 500 克", + "notes": "量未指定" + }, + { + "name": "商芝:", + "quantity": null, + "unit": null, + "text_quantity": "- 商芝: 50 克", + "notes": "量未指定" + }, + { + "name": "葱:", + "quantity": null, + "unit": null, + "text_quantity": "- 葱: 10 克", + "notes": "量未指定" + }, + { + "name": "姜:", + "quantity": null, + "unit": null, + "text_quantity": "- 姜: 2 克", + "notes": "量未指定" + }, + { + "name": "八角:", + "quantity": null, + "unit": null, + "text_quantity": "- 八角: 3 个", + "notes": "量未指定" + }, + { + "name": "蜂蜜:", + "quantity": null, + "unit": null, + "text_quantity": "- 蜂蜜: 15 克", + "notes": "量未指定" + }, + { + "name": "醋:", + "quantity": null, + "unit": null, + "text_quantity": "- 醋: 5 克", + "notes": "量未指定" + }, + { + "name": "料酒:", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒: 15 克", + "notes": "量未指定" + }, + { + "name": "味精:", + "quantity": null, + "unit": null, + "text_quantity": "- 味精: 1.5 克", + "notes": "量未指定" + }, + { + "name": "酱油:", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油: 10 克", + "notes": "量未指定" + }, + { + "name": "摊鸡蛋皮:一张约", + "quantity": null, + "unit": null, + "text_quantity": "- 摊鸡蛋皮:一张约 15 克", + "notes": "量未指定" + }, + { + "name": "精盐:", + "quantity": null, + "unit": null, + "text_quantity": "- 精盐: 1 克", + "notes": "量未指定" + }, + { + "name": "鸡汤:", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡汤: 200 克", + "notes": "量未指定" + }, + { + "name": "芝麻油:", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油: 10 克", + "notes": "量未指定" + }, + { + "name": "熟猪油:", + "quantity": null, + "unit": null, + "text_quantity": "- 熟猪油: 2000 克(消耗约 60 克)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将肉刮洗干净,入煮锅煮至六成熟(变色为白),捞出趁热用蜂蜜、醋涂抹肉皮。" + }, + { + "step": 2, + "description": "炒锅内放入熟猪油,用旺火烧至八成熟(约 200 度,油表有大量青烟,油状平静),将肉块皮朝下投入,炸至呈金红色时,捞入凉肉煮锅(之前煮完的煮锅)中泡软,放在案板上,切成三寸(10 cm)长、两分(0.6 cm)厚的片,仍然皮朝下,整齐装入蒸碗内。" + }, + { + "step": 3, + "description": "将 5 克大葱切成 2.4 cm 长的段,5 克切成 2.4 cm 长的斜形片。姜去皮洗净,1.5 克切成片,5 克切成末,摊的鸡蛋皮切成 2.4 cm 长的等腰三角形片。" + }, + { + "step": 4, + "description": "商芝入沸水锅中煮软捞出,去除老茎、杂质,淘洗干净,切成 3 cm 长的段,放入碗中,加酱油(5 克)、精盐(1 克)、熟猪油(10 克)拌匀,盖在肉片上,另将鸡汤(100 克)放入一小碗中,加酱油(5 克)、精盐(0.5 克)、料酒(15 克)搅匀,浇入蒸碗,再放入姜片、葱段、八角上笼用旺火蒸约半小时后,转用小火继续蒸约一小时三十分钟,熟烂后取出,拣去姜、葱、八角,倒、过滤原汁,将肉扣入汤盘。" + }, + { + "step": 5, + "description": "炒锅内,放入鸡汤(100 克),加入原汁,用旺火烧沸,下入姜末、葱片、味精后搅匀,投入摊鸡蛋皮,淋芝麻油,浇入汤盘即成。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-孜然牛肉", + "name": "孜然牛肉的做法", + "description": "# 孜然牛肉的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/孜然牛肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛柳或牛肩肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛柳或牛肩肉", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "孜然(颗粒>粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然(颗粒>粉)", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "捣药罐(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 捣药罐(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "首先将小米椒切碎,和孜然粒一起放入捣药罐捣碎成颗粒,这样更入味。如果时间紧张可跳过捣碎步骤" + }, + { + "step": 2, + "description": "青椒切头去籽(喜欢辣的可不去),切成丝。葱切段。" + }, + { + "step": 3, + "description": "牛肉提前解冻,过一边水洗干净,晾干或用厨用纸吸干,将牛肉顺着纹理切成片" + }, + { + "step": 4, + "description": "然后进行腌肉,加入生抽,淀粉,油,均匀搅拌,静止 30 分钟。腌肉方法也可参考[学习腌](../../tips/learn/学习腌.md)" + }, + { + "step": 5, + "description": "热锅下油,放入葱,爆出香味后放入腌好的牛肉煸炒" + }, + { + "step": 6, + "description": "牛肉变色后均匀放入孜然辣椒颗粒并炒熟" + }, + { + "step": 7, + "description": "然后下入青椒丝,断生后放盐" + }, + { + "step": 8, + "description": "大🔥炒 1 分钟后关火再翻炒 30 秒保证受热均匀即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-小炒肉", + "name": "小炒肉的做法", + "description": "# 小炒肉的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/小炒肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "朝天椒", + "quantity": null, + "unit": null, + "text_quantity": "- 朝天椒", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "豆豉", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 500g", + "notes": "量未指定" + }, + { + "name": "朝天椒", + "quantity": null, + "unit": null, + "text_quantity": "- 朝天椒 4 条", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 4 颗", + "notes": "量未指定" + }, + { + "name": "豆豉", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉 10g,根据个人口味加减 ±5g", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 10g,根据个人口味加减 ±5g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10ml", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 10g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1-2g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 0.5-1 根", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2 瓣", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "五花肉切片" + }, + { + "step": 2, + "description": "把肉放入器皿内,加入淀粉、老抽、盐搅拌腌制半小时" + }, + { + "step": 3, + "description": "葱切段" + }, + { + "step": 4, + "description": "小米椒、朝天椒斜刀切好" + }, + { + "step": 5, + "description": "热锅、倒油" + }, + { + "step": 6, + "description": "油热后加入五花肉煸炒。炒至变色后盛出来" + }, + { + "step": 7, + "description": "向锅中加蒜,煸出香味,加入豆豉,翻炒均匀" + }, + { + "step": 8, + "description": "加入豆瓣酱翻炒均匀" + }, + { + "step": 9, + "description": "加入炒好的五花肉继续的翻炒均匀" + }, + { + "step": 10, + "description": "加入小米椒、朝天椒、葱段翻炒 40 秒" + }, + { + "step": 11, + "description": "出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-小米辣炒肉", + "name": "小米辣炒肉的做法", + "description": "# 小米辣炒肉的做法\n\n⚠️注意:不建议清淡饮食的尝试。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/小米辣炒肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油", + "notes": "量未指定" + }, + { + "name": "五花肉/瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉/瘦肉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 20 个,根据个人口味加减", + "notes": "量未指定" + }, + { + "name": "花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油 20ml", + "notes": "量未指定" + }, + { + "name": "五花肉/瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉/瘦肉 200g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1-2g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 10ml", + "notes": "量未指定" + }, + { + "name": "姜蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜蒜 50g", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 10g,根据个人口味加减", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 10g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将小米辣洗净,斜刀切大一点" + }, + { + "step": 2, + "description": "肉的话,想切丝切丝,想切片切片,倒入调料(生抽、蚝油、盐)腌制 5 分钟" + }, + { + "step": 3, + "description": "热锅倒油,先把肉炒好盛起" + }, + { + "step": 4, + "description": "姜蒜爆香,倒入豆瓣酱翻炒,到入切好的小米辣,再倒入瘦肉,翻炒一下,放点生抽、鸡精、盐、糖翻炒" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-小酥肉", + "name": "小酥肉的做法", + "description": "# 小酥肉的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/小酥肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "去皮猪肉(根据喜好选择肥瘦)", + "quantity": null, + "unit": null, + "text_quantity": "- 去皮猪肉(根据喜好选择肥瘦)", + "notes": "量未指定" + }, + { + "name": "植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油", + "notes": "量未指定" + }, + { + "name": "老姜", + "quantity": null, + "unit": null, + "text_quantity": "- 老姜", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "花椒碎", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒碎", + "notes": "量未指定" + }, + { + "name": "花椒粒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒粒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "红薯淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 红薯淀粉", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "老姜切丝,小葱不用切。" + }, + { + "step": 2, + "description": "根据计算公式倒入料酒、清水。" + }, + { + "step": 3, + "description": "用手捏揉 5 分钟,使姜葱的味道充分溶解在水中。" + }, + { + "step": 4, + "description": "将猪肉去皮洗净" + }, + { + "step": 5, + "description": "切成长度 8~10 厘米,厚度 1.5 厘米的肉条。" + }, + { + "step": 6, + "description": "根据上面的计算公式加入盐,十三香、胡椒粉、味精、鸡精、花椒碎、花椒粒、生抽。" + }, + { + "step": 7, + "description": "倒入前面制作好的葱姜水" + }, + { + "step": 8, + "description": "抓匀并且充分揉制 10 分钟,直至肉吸收所有水分并且变得粘手。" + }, + { + "step": 9, + "description": "封上保鲜膜放冷藏室静置 30 分钟。" + }, + { + "step": 10, + "description": "将面粉、红薯粉倒入腌制好的肉中,加入鸡蛋清。" + }, + { + "step": 11, + "description": "充分揉制 15 分钟。" + }, + { + "step": 12, + "description": "锅中倒入植物油,根据锅大小控制油量,油面高度 3 厘米以上。" + }, + { + "step": 13, + "description": "大火将温加热至 150° 后,转小火保持温度。" + }, + { + "step": 14, + "description": "将裹好粉的肉条用筷子夹入油锅中,捋成自己喜欢的形状,炸 3~5 分钟定型。目测颜色微黄,用锅铲翻动感受倒略微有些硬了就可以。具体时间受肉块大小、油温、裹粉程度影响。" + }, + { + "step": 15, + "description": "捞出沥油。" + }, + { + "step": 16, + "description": "将油温升至 180° 放入初炸好的肉条,炸至金黄色即可捞出。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-尖椒炒牛肉", + "name": "尖椒炒牛肉的做法", + "description": "# 尖椒炒牛肉的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/尖椒炒牛肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉", + "notes": "量未指定" + }, + { + "name": "葱、姜、蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱、姜、蒜", + "notes": "量未指定" + }, + { + "name": "尖椒", + "quantity": null, + "unit": null, + "text_quantity": "- 尖椒", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "蒜剁成蒜泥" + }, + { + "step": 2, + "description": "葱切段" + }, + { + "step": 3, + "description": "姜切成姜片" + }, + { + "step": 4, + "description": "尖椒切成段" + }, + { + "step": 5, + "description": "牛肉放入碗中" + }, + { + "step": 6, + "description": "加姜、盐、酱油、糖进行腌制 30-40 分钟" + }, + { + "step": 7, + "description": "腌制完姜可以去掉" + }, + { + "step": 8, + "description": "冷油下锅,待油变热至偶有气泡" + }, + { + "step": 9, + "description": "加入蒜泥" + }, + { + "step": 10, + "description": "蒜泥变金黄后加入尖椒" + }, + { + "step": 11, + "description": "待尖椒表皮微皱,加入腌制好的牛肉翻炒" + }, + { + "step": 12, + "description": "翻炒变熟之前加入葱,继续翻炒" + }, + { + "step": 13, + "description": "翻炒至牛肉变熟,关火出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-山西过油肉", + "name": "山西过油肉的做法", + "description": "# 山西过油肉的做法\n\n过油肉是山西传统名菜,有很多年历史,基本家家都会做。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/山西过油肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪里脊", + "quantity": null, + "unit": null, + "text_quantity": "- 猪里脊", + "notes": "量未指定" + }, + { + "name": "蒜苔", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苔", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "葱姜蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜蒜", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "木耳", + "quantity": null, + "unit": null, + "text_quantity": "- 木耳", + "notes": "量未指定" + }, + { + "name": "葱头", + "quantity": null, + "unit": null, + "text_quantity": "- 葱头", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "陈醋", + "quantity": null, + "unit": null, + "text_quantity": "- 陈醋", + "notes": "量未指定" + }, + { + "name": "花椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒粉", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "猪里脊", + "quantity": null, + "unit": null, + "text_quantity": "- 猪里脊 150 g", + "notes": "量未指定" + }, + { + "name": "蒜苔", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苔 6 根", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 300ml", + "notes": "量未指定" + }, + { + "name": "葱姜蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜蒜 50g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 20ml", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 10g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 10g", + "notes": "量未指定" + }, + { + "name": "木耳", + "quantity": null, + "unit": null, + "text_quantity": "- 木耳 20g", + "notes": "量未指定" + }, + { + "name": "葱头", + "quantity": null, + "unit": null, + "text_quantity": "- 葱头 100g", + "notes": "量未指定" + }, + { + "name": "其他调料", + "quantity": null, + "unit": null, + "text_quantity": "- 其他调料 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "木耳提前泡发好,如果着急可以用热水泡发。" + }, + { + "step": 2, + "description": "猪里脊切片放入碗中,加 20ml 生抽、料酒、花椒粉,打入一个鸡蛋,拿自己的小手搅拌均匀,加入淀粉(建议红薯淀粉)拌匀,倒入 300ml 食用油封浆,腌制 15 分钟。" + }, + { + "step": 3, + "description": "蒜苔切段大约 3cm,葱头切菱形块备用。" + }, + { + "step": 4, + "description": "起锅烧油油要多一点,油温五成热,下入腌制好的肉片,将肉片打散,捞出控油备用。" + }, + { + "step": 5, + "description": "将锅中多余油倒出,留 10ml 油炒菜,油温七成热" + }, + { + "step": 6, + "description": "下入葱姜蒜爆香,先下蒜苔炒至断生,再下入木耳葱头,加入生抽,花椒粉,翻炒几下将之前炸好的肉片下入翻炒" + }, + { + "step": 7, + "description": "加 10g 的盐,起锅前加 10ml 的醋和鸡精,起锅装盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-带把肘子", + "name": "带把肘子的做法", + "description": "# 带把肘子的做法\n\n肘肉酥烂不腻,肘皮胶粘,香醇味美,辅佐以葱段,甜面酱,别有一番风味,因脚爪形似把柄,故得其名,是陕西省大荔县名菜。营养价值丰富,但制作难度较高。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/带把肘子.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "带脚、爪猪前肘: 一个(大约二斤五两 =", + "quantity": null, + "unit": null, + "text_quantity": "- 带脚、爪猪前肘: 一个(大约二斤五两 = 1250 克)", + "notes": "量未指定" + }, + { + "name": "红豆腐乳:", + "quantity": null, + "unit": null, + "text_quantity": "- 红豆腐乳: 1 块 = 10 克", + "notes": "量未指定" + }, + { + "name": "甜面酱:", + "quantity": null, + "unit": null, + "text_quantity": "- 甜面酱: 150 克", + "notes": "量未指定" + }, + { + "name": "精盐:", + "quantity": null, + "unit": null, + "text_quantity": "- 精盐: 15 克", + "notes": "量未指定" + }, + { + "name": "红酱油:", + "quantity": null, + "unit": null, + "text_quantity": "- 红酱油: 35 克", + "notes": "量未指定" + }, + { + "name": "白酱油:", + "quantity": null, + "unit": null, + "text_quantity": "- 白酱油: 25 克", + "notes": "量未指定" + }, + { + "name": "料酒:", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒: 25 克", + "notes": "量未指定" + }, + { + "name": "蒜片:", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜片: 50 克", + "notes": "量未指定" + }, + { + "name": "姜末:", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末: 10 克", + "notes": "量未指定" + }, + { + "name": "八角:", + "quantity": null, + "unit": null, + "text_quantity": "- 八角: 3 个", + "notes": "量未指定" + }, + { + "name": "桂皮:", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮: 5 克", + "notes": "量未指定" + }, + { + "name": "葱:", + "quantity": null, + "unit": null, + "text_quantity": "- 葱: 200 克", + "notes": "量未指定" + }, + { + "name": "带脚、爪猪前肘", + "quantity": null, + "unit": null, + "text_quantity": "- 带脚、爪猪前肘", + "notes": "量未指定" + }, + { + "name": "红豆腐乳", + "quantity": null, + "unit": null, + "text_quantity": "- 红豆腐乳", + "notes": "量未指定" + }, + { + "name": "甜面酱", + "quantity": null, + "unit": null, + "text_quantity": "- 甜面酱", + "notes": "量未指定" + }, + { + "name": "精盐", + "quantity": null, + "unit": null, + "text_quantity": "- 精盐", + "notes": "量未指定" + }, + { + "name": "红酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 红酱油", + "notes": "量未指定" + }, + { + "name": "白酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 白酱油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "蒜片", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜片", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将肘子刮洗干净,肘头朝外、肘把(脚爪)朝里、肘皮朝下放在案板上。" + }, + { + "step": 2, + "description": "用刀在正中由肘头向肘把沿着腿骨将皮剖开,剔去腿骨两边的肉(三面离肉),底部骨与肉相连,使骨头露出,然后将两节腿骨由中间用刀背(还是用斧头吧)砸断。" + }, + { + "step": 3, + "description": "肘子放入煮锅煮至七成熟捞出(外观正常,内部淡红色),用干净抹布擦干水,趁热用红酱油涂抹肉皮。" + }, + { + "step": 4, + "description": "取蒸锅一个,锅底放入八角、桂皮,先将肘把的关节处用手掰断,不伤外皮,再将肘皮朝下装进蒸锅内,装锅时根据肘子体型,将肘把贴住锅边窝着装进锅内,成为圆形。" + }, + { + "step": 5, + "description": "撒入精盐,用消过毒的干净纱布盖在肉上,再将甜面酱(50 克)、葱(75 克)、红豆腐乳、红酱油、白酱油、姜、蒜等在纱布上抹开,用旺火蒸大约三小时(以蒸烂为准)。" + }, + { + "step": 6, + "description": "蒸完取出,揭去纱布,扣入盘中,拣去八角,上桌时另带葱段和甜面酱小碟(或将甜面酱抹到肘面上,另带葱段小碟亦可)。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-意式烤鸡", + "name": "意式烤鸡的做法", + "description": "# 意式烤鸡的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/意式烤鸡.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡腿肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿肉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "黑胡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "柠檬汁", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬汁", + "notes": "量未指定" + }, + { + "name": "欧芹", + "quantity": null, + "unit": null, + "text_quantity": "- 欧芹", + "notes": "量未指定" + }, + { + "name": "鸡腿肉用量通常来说为", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿肉用量通常来说为 1-2 个/人", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡腿肉涂上盐、黑胡椒、橄榄油和蒜末" + }, + { + "step": 2, + "description": "放入预热至 180 度的烤箱中,烤 30-40 分钟或至熟" + }, + { + "step": 3, + "description": "欧芹切成碎末备用" + }, + { + "step": 4, + "description": "柠檬挤出汁备用" + }, + { + "step": 5, + "description": "烤好的鸡肉取出,淋上柠檬汁" + }, + { + "step": 6, + "description": "撒上欧芹碎即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-杀猪菜", + "name": "杀猪菜的做法", + "description": "# 杀猪菜的做法\n\n杀猪菜的做法 (荤菜)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/杀猪菜.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "血肠", + "quantity": null, + "unit": null, + "text_quantity": "- 血肠", + "notes": "量未指定" + }, + { + "name": "酸菜", + "quantity": null, + "unit": null, + "text_quantity": "- 酸菜", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "葱结", + "quantity": null, + "unit": null, + "text_quantity": "- 葱结", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "血肠", + "quantity": null, + "unit": null, + "text_quantity": "- 血肠 200 克", + "notes": "量未指定" + }, + { + "name": "酸菜", + "quantity": null, + "unit": null, + "text_quantity": "- 酸菜 500 克", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨 400 克", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10 克", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 5 个", + "notes": "量未指定" + }, + { + "name": "姜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 姜粉 5 克", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 5 个", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 2 片", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 1 个", + "notes": "量未指定" + }, + { + "name": "葱结", + "quantity": null, + "unit": null, + "text_quantity": "- 葱结 1 个", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 10 克", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油 10 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5 克", + "notes": "量未指定" + }, + { + "name": "蘸料:辣椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 蘸料:辣椒油 5 克、生抽 10 克、蒜蓉 5 克、香油 2 克。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "血肠用牙签多扎一些小孔,然后放水中小火煮十分钟,不要让水烧开,保持 80 度,否则血肠非常容易爆开。" + }, + { + "step": 2, + "description": "煮好的血肠切块备用。" + }, + { + "step": 3, + "description": "排骨放料酒焯水,控干水分备用。" + }, + { + "step": 4, + "description": "锅内放入菜籽油,放蒜瓣,干辣椒,姜粉炒香。" + }, + { + "step": 5, + "description": "放入排骨翻炒至表面金黄。" + }, + { + "step": 6, + "description": "酸菜洗净拧干水分,放入锅中,加入香油翻炒,香油可以更好的去除酸味而且让酸菜更香,大火翻炒二分钟。" + }, + { + "step": 7, + "description": "加入 600 毫升热水。" + }, + { + "step": 8, + "description": "转入电压力锅,加香叶,八角,葱结,盐。" + }, + { + "step": 9, + "description": "浓香模式压 40 分钟。" + }, + { + "step": 10, + "description": "到时间后放气开盖。加入血肠和枸杞,盖上锅盖焖二分钟即可,血肠是熟的,不需再加热。" + }, + { + "step": 11, + "description": "倒入盆中,按照上表调制蘸料,即可开吃。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-椒盐排条", + "name": "椒盐排条的做法", + "description": "# 椒盐排条的做法\n\n椒盐排条是道非常经典的本帮菜,咸、香,也容易制作。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/椒盐排条.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "大排", + "quantity": null, + "unit": null, + "text_quantity": "- 大排", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "椒盐粉", + "quantity": null, + "unit": null, + "text_quantity": "- 椒盐粉", + "notes": "量未指定" + }, + { + "name": "葱姜水", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜水", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "吉士粉(增色增香,没有可以不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 吉士粉(增色增香,没有可以不放)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "大排", + "quantity": null, + "unit": null, + "text_quantity": "- 大排 4 块(大约 360 克)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个 50 g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1 g", + "notes": "量未指定" + }, + { + "name": "椒盐粉", + "quantity": null, + "unit": null, + "text_quantity": "- 椒盐粉 10 g", + "notes": "量未指定" + }, + { + "name": "葱姜水", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜水 100 ml", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 80 g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 80 g", + "notes": "量未指定" + }, + { + "name": "吉士粉", + "quantity": null, + "unit": null, + "text_quantity": "- 吉士粉 2-3 g(增色增香,没有可以不放)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 10 g", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 10 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "大排洗干净,剔骨,用刀面拍松,切成厚片,再改成粗条。" + }, + { + "step": 2, + "description": "加入椒盐粉,搅匀,待到出胶质了**分次**加入葱姜水,放入冰箱腌制 20 分钟。" + }, + { + "step": 3, + "description": "制作炸糊。放入 80 g 面粉,20 g 淀粉(注意是 20 g 淀粉,剩下 60 g 备用),2 - 3 g 吉士粉,盐 1 g。" + }, + { + "step": 4, + "description": "打入一个鸡蛋,搅拌,再分次加入水 100 g ,再加 10 g 油,反复搅拌。直到炸糊完全调开,略粘稠即可。" + }, + { + "step": 5, + "description": "取出剩余的 60 g 淀粉,取出排条,裹上一层淀粉,再裹上面糊。" + }, + { + "step": 6, + "description": "锅中加入油,能没过食材即可,加热到大约 150 ℃ - 160 ℃ 。下入排条炸成浅金黄色后捞出。刚下入排条时可能会有粘连,不要动。待排条定型后可用筷子翻动,即可分开。" + }, + { + "step": 7, + "description": "待油温再次升高到 150 ℃ - 160 ℃ 时,下入排条复炸至金黄色后捞出。" + }, + { + "step": 8, + "description": "撒上椒盐粉,搅拌均匀后出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。", + "做法参考:[椒盐排条的做法](https://www.bilibili.com/video/BV14s4y1c76H)" + ] + }, + { + "id": "dishes-meat_dish-水煮肉片", + "name": "水煮肉片的做法", + "description": "# 水煮肉片的做法\n\n水煮肉片麻辣鲜香,适合干饭,但是做法稍微有点麻烦。难度主要在肉滑嫩,初学者一般需要 1 - 2 小时完成。干饭人,一切都值~\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/水煮肉片.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪里脊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪里脊肉", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "鸡蛋清", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋清", + "notes": "量未指定" + }, + { + "name": "土豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆淀粉", + "notes": "量未指定" + }, + { + "name": "植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油", + "notes": "量未指定" + }, + { + "name": "豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 豆芽", + "notes": "量未指定" + }, + { + "name": "凤尾", + "quantity": null, + "unit": null, + "text_quantity": "- 凤尾", + "notes": "量未指定" + }, + { + "name": "芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "红泡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红泡椒", + "notes": "量未指定" + }, + { + "name": "青花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "红油豆瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 红油豆瓣", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 2 根", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 10g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 20g", + "notes": "量未指定" + }, + { + "name": "红泡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红泡椒 20g(根据受辣程度选择 0-40 g)", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗 2 根", + "notes": "量未指定" + }, + { + "name": "芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜 3 根", + "notes": "量未指定" + }, + { + "name": "红油瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 红油瓣酱 5ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 1.5g", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油 5g", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 5g", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 2g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 3g", + "notes": "量未指定" + }, + { + "name": "鸡蛋清", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋清 1 个", + "notes": "量未指定" + }, + { + "name": "土豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆淀粉 7g", + "notes": "量未指定" + }, + { + "name": "植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油 280g(根据情况选择,想吃重油就多加 100g)", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油 200g(根据情况选择,想吃重油就多加 100g)", + "notes": "量未指定" + }, + { + "name": "绿豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 绿豆芽 100g", + "notes": "量未指定" + }, + { + "name": "凤尾", + "quantity": null, + "unit": null, + "text_quantity": "- 凤尾 1 根", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 1g", + "notes": "量未指定" + }, + { + "name": "小米辣干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣干辣椒 20g(根据受辣程度选择 0-40g)", + "notes": "量未指定" + }, + { + "name": "青花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青花椒 5g(根据情况选择,想吃麻就多 5g)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "里脊肉改刀成小块,再切成 2 毫米薄片(可根据自己的口感改进),放入碗中,加入清水清洗两遍来去除血水和杂质,捞出挤干水分备用。" + }, + { + "step": 2, + "description": "碗中加入食用盐 1.5g,胡椒粉 1g,生抽酱油 5g,料酒 3g,然后朝着一个方向搅拌 2 分钟,使其入味。" + }, + { + "step": 3, + "description": "另外准备一个碗,加入一个鸡蛋清,加入 7g 土豆淀粉,一个方向搅拌均匀,倒入肉中" + }, + { + "step": 4, + "description": "绿豆芽 100g,凤尾 1 根(改刀成小条),芹菜 3 根切成小段,蒜苗 2 根拍散切成小段。" + }, + { + "step": 5, + "description": "大蒜 20g 剁碎,生姜小块剁碎,红泡椒 20g 剁碎。" + }, + { + "step": 6, + "description": "小米辣干辣椒 15g,青花椒 3g,锅内加入油滑锅,油稍许热了将多余的倒出备用留 50g 底油,下入干辣椒、花椒,开小火炒香,切记不要炒糊(颜色要变黑即可),倒出在菜板上剁细。" + }, + { + "step": 7, + "description": "锅烧热,放入 100g 植物油烧至 6 成热,加入 2g 青花椒、干辣椒爆香,配菜下锅,加入 1g 食用盐,炒至断生,盛入碗中垫底备用。" + }, + { + "step": 8, + "description": "锅洗干净,加入 150g 植物油烧至 6 成热,加入制作好的姜蒜红泡椒,爆香后加入豆瓣 10g,开小火把豆瓣爆香炒出红油即可。" + }, + { + "step": 9, + "description": "加入 800 毫升清水(根据实际情况选择),大火烧开,转小火调味,加入食用盐 2.5g,鸡精 1.5g,1g 白砂糖提鲜,1g 胡椒粉,5g 水淀粉(根据实际情况选择)将汤汁收浓稠一点。" + }, + { + "step": 10, + "description": "汤汁开后,开小火将腌制好的肉片分开依次下锅,然后开中火将肉片烫熟,用锅铲轻轻推动一下避免粘连,待汤汁烧开,肉片熟后捞出放入碗中配菜上,再将原汤倒入(不超过菜品)。" + }, + { + "step": 11, + "description": "碗中均匀撒上刀口辣椒、蒜蓉和葱花。" + }, + { + "step": 12, + "description": "锅洗干净,加入 200g 菜籽油,烧至 7 成热,然后一次性均匀泼在碗中肉片上(注意安全),美味完成。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-洋葱炒猪肉", + "name": "洋葱炒猪肉的做法", + "description": "# 洋葱炒猪肉的做法\n\n咸中带甜,简单上手,一不小心可能让人多吃一碗饭。一般只需 15 分钟即可完成。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/洋葱炒猪肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "猪肉片", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉片", + "notes": "量未指定" + }, + { + "name": "蕃茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蕃茄酱", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油", + "notes": "量未指定" + }, + { + "name": "洋葱 一颗 (是主角,喜欢吃洋葱可以多半颗~一颗)", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 一颗 (是主角,喜欢吃洋葱可以多半颗~一颗)", + "notes": "量未指定" + }, + { + "name": "猪肉 (250g)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉 (250g)", + "notes": "量未指定" + }, + { + "name": "蒜头 (3 瓣)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 (3 瓣)", + "notes": "量未指定" + }, + { + "name": "食用油 (15ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 (15ml)", + "notes": "量未指定" + }, + { + "name": "黑胡椒 (1.25g)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒 (1.25g)", + "notes": "量未指定" + }, + { + "name": "酱油 (30ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 (30ml)", + "notes": "量未指定" + }, + { + "name": "糖 (15g)", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 (15g)", + "notes": "量未指定" + }, + { + "name": "麻油 (5ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油 (5ml)", + "notes": "量未指定" + }, + { + "name": "番茄酱 (15ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 (15ml)", + "notes": "量未指定" + }, + { + "name": "料酒 (15ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 (15ml)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "洋葱切片,猪肉,蒜头拍碎,以及混合上述调味料备用" + }, + { + "step": 2, + "description": "炒锅内倒入 1 大匙食用油(等待 10 秒让油温升高),倒入猪肉" + }, + { + "step": 3, + "description": "炒至变色后下蒜头炒香盛起备用" + }, + { + "step": 4, + "description": "原锅下洋葱翻炒 3~4 分钟后加入调味料炒匀" + }, + { + "step": 5, + "description": "下刚盛起备用的猪肉翻炒至猪肉熟后" + }, + { + "step": 6, + "description": "待猪肉熟后再翻炒 1、2 分钟即可起锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-烤鸡翅", + "name": "烤鸡翅的做法", + "description": "# 烤鸡翅的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/烤鸡翅.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡翅中", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡翅中", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "烤箱", + "quantity": null, + "unit": null, + "text_quantity": "- 烤箱", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡翅放入碗中" + }, + { + "step": 2, + "description": "加盐、黑胡椒粉、酱油、料酒进行腌制 30-40 分钟" + }, + { + "step": 3, + "description": "将烤箱预热至 200℃" + }, + { + "step": 4, + "description": "将腌制好的鸡翅均匀地放在烤盘上" + }, + { + "step": 5, + "description": "将烤盘放入烤箱中层,烤 15-20 分钟" + }, + { + "step": 6, + "description": "取出烤盘,将鸡翅翻面,再烤 15-20 分钟,直到熟透" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-猪肉烩酸菜", + "name": "猪肉烩酸菜的做法", + "description": "# 猪肉烩酸菜的做法\n\n猪肉烩酸菜是一道北方名菜,简单易做。富含蛋白质。一般初学者需要 3 小时完成。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/猪肉烩酸菜.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪五花肉或猪肉排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 猪五花肉或猪肉排骨", + "notes": "量未指定" + }, + { + "name": "东北酸菜", + "quantity": null, + "unit": null, + "text_quantity": "- 东北酸菜", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "大料", + "quantity": null, + "unit": null, + "text_quantity": "- 大料", + "notes": "量未指定" + }, + { + "name": "猪排骨或者五花肉(总共)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪排骨或者五花肉(总共) 1500 克", + "notes": "量未指定" + }, + { + "name": "东北酸菜", + "quantity": null, + "unit": null, + "text_quantity": "- 东北酸菜 1000 克", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 1 根", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 100 克", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 4 瓣", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 10 克", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油 15 克", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 10 克", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 20 毫升", + "notes": "量未指定" + }, + { + "name": "大料", + "quantity": null, + "unit": null, + "text_quantity": "- 大料 2 颗", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "大葱切段;生姜 50 克切段, 50 克切末;大蒜切末,备用。" + }, + { + "step": 2, + "description": "全部酸菜切丝,用水冲洗 2 ~ 3 遍备用。" + }, + { + "step": 3, + "description": "排骨和五花肉入锅,倒入冷水淹没。放入全部葱段, 50 克生姜段和料酒 20 毫升。大火煮开后,等待 5 分钟。关火,将排骨和五花肉捞出,冷水冲洗掉浮沫,备用" + }, + { + "step": 4, + "description": "煮好的五花肉切片或者切块,备用。" + }, + { + "step": 5, + "description": "将之前的锅洗干净,并且擦干(不然加入油会崩出来)。" + }, + { + "step": 6, + "description": "锅中加入油,开中火,放入姜蒜末爆香,放入五花肉和排骨。将五花肉和排骨煎至金黄,倒入 10 克五香粉和 15 克 生抽酱油,用铲子翻动 1 ~ 2 分钟。" + }, + { + "step": 7, + "description": "将冲洗好的酸菜丝加入锅中,翻炒 3 分钟。" + }, + { + "step": 8, + "description": "倒入纯净水至刚好没过食材,加入 2 颗大料,转大火,直到锅中水沸腾。转中火,盖锅盖焖煮。" + }, + { + "step": 9, + "description": "等待 1.5 ~ 2 小时,直至五花肉软烂 (可以用筷子轻松扎穿)" + }, + { + "step": 10, + "description": "掀开锅盖,开大火收汤,翻动锅中食材直至锅中剩余水分只覆盖锅底,转小火,准备调味。" + }, + { + "step": 11, + "description": "调味:加入食用盐 10 克,搅拌均匀。" + }, + { + "step": 12, + "description": "关火,出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-甜辣烤全翅", + "name": "甜辣烤全翅的做法", + "description": "# 甜辣烤全翅的做法\n\n本甜辣烤全翅使用空气炸锅烹饪并仅使用家中常见调料,低油脂并且不需要成品烧烤酱,一份适合单人食用,食材处理需要 15 分钟,腌制需要 120 分钟, 烹饪需要 50 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/甜辣烤全翅.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "空气炸锅", + "quantity": null, + "unit": null, + "text_quantity": "- 空气炸锅", + "notes": "量未指定" + }, + { + "name": "鸡全翅", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡全翅", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "蒜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜粉", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "甜椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 甜椒粉", + "notes": "量未指定" + }, + { + "name": "辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒粉", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "锡纸盘", + "quantity": null, + "unit": null, + "text_quantity": "- 锡纸盘", + "notes": "量未指定" + }, + { + "name": "保鲜膜", + "quantity": null, + "unit": null, + "text_quantity": "- 保鲜膜", + "notes": "量未指定" + }, + { + "name": "鸡全翅", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡全翅 4 个", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 45ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 15ml", + "notes": "量未指定" + }, + { + "name": "蒜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜粉 10g", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 5g", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 10g", + "notes": "量未指定" + }, + { + "name": "甜椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 甜椒粉 10g", + "notes": "量未指定" + }, + { + "name": "辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒粉 5g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 15ml", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 20ml", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 10ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 4 个新鲜鸡全翅取出,在翅中两根骨头之间用刀划开表皮,正反面各一刀" + }, + { + "step": 2, + "description": "将 4 个鸡全翅放入碗中,加入生抽 45ml , 老抽 15ml , 蒜粉 10g , 胡椒粉 5g , 糖 10g , 甜椒粉 10g ,辣椒粉 5g , 蚝油 15ml , 水 20ml 以及油 10ml" + }, + { + "step": 3, + "description": "用勺子将酱汁均匀的抹在鸡全翅上,尤其是翅中的刀口处,大约花费 3 分钟" + }, + { + "step": 4, + "description": "用保鲜膜盖住防油腌制中鸡全翅的碗,放入冰箱冷藏格静置 120 分钟" + }, + { + "step": 5, + "description": "取出鸡全翅,锡纸盘中放入鸡全翅 4 个,将碗中残余酱料均匀倒在鸡全翅上" + }, + { + "step": 6, + "description": "锡纸盘放入空气炸锅的烤篮上,用 200 摄氏度烤 25 分钟" + }, + { + "step": 7, + "description": "打开空气炸锅,小心取出锡纸盘,将鸡全翅翻面" + }, + { + "step": 8, + "description": "继续 200 摄氏度烤 25 分钟" + }, + { + "step": 9, + "description": "取出即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-番茄红酱", + "name": "番茄红酱的做法", + "description": "# 番茄红酱的做法\n\n番茄红酱香浓可口,营养丰富,咱很喜欢。可以作为薄饼、意面~~热干面~~等主食的百搭酱料。有些繁琐,适合有烹饪经验的人尝试。一次吃不完也没有关系,可以冷冻后随时拿出来加热哦。(但是千万要记得吃)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/番茄红酱.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "碎牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 碎牛肉", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜", + "notes": "量未指定" + }, + { + "name": "芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "干罗勒或百里香(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 干罗勒或百里香(可选)", + "notes": "量未指定" + }, + { + "name": "碎牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 碎牛肉 500g", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 2 个", + "notes": "量未指定" + }, + { + "name": "胡萝卜 半根", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 半根", + "notes": "量未指定" + }, + { + "name": "芹菜 一根", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜 一根", + "notes": "量未指定" + }, + { + "name": "洋葱 半个", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 半个", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 2g", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 10g", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉 5g", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 300g", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 300ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将胡萝卜、芹菜、洋葱切碎,蒜瓣切片。" + }, + { + "step": 2, + "description": "加入 10ml 橄榄油,热油下锅蔬菜,大火翻炒开始略微变色后盛出。" + }, + { + "step": 3, + "description": "锅内加油 10ml,加蒜翻炒 10 秒,加入碎牛肉、糖、盐、胡椒粉和香料将牛肉炒脆(有颗粒感)。" + }, + { + "step": 4, + "description": "加入炒好的蔬菜们和番茄酱继续翻炒,搅拌均匀。" + }, + { + "step": 5, + "description": "分多次缓缓倒入牛奶,中小火煮 30 分钟,完成。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-白菜猪肉炖粉条", + "name": "白菜猪肉炖粉条的做法", + "description": "# 白菜猪肉炖粉条的做法\n\n白菜猪肉炖粉条是一道简单易做的菜。这是一道传统的东北家常菜,以做法简单、味道上乘的特点,在广大东北人民群众中备受喜爱。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/白菜猪肉炖粉条.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "白菜", + "quantity": null, + "unit": null, + "text_quantity": "- 白菜", + "notes": "量未指定" + }, + { + "name": "土豆干粉条", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆干粉条", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 300g", + "notes": "量未指定" + }, + { + "name": "大白菜", + "quantity": null, + "unit": null, + "text_quantity": "- 大白菜 500g", + "notes": "量未指定" + }, + { + "name": "土豆干粉条", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆干粉条 50g", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香 10g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 5g", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 15g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅内烧水,水开后放入干粉条,煮 5 分钟后同水一起倒出容器中,盖上盖子继续浸泡泡 备用(第一步先做这个,期间可以进行以下步骤)" + }, + { + "step": 2, + "description": "五花肉切 3mm 的肉片,备用" + }, + { + "step": 3, + "description": "大白菜嫩叶与白菜帮子分开切成 2 份菜片,备用" + }, + { + "step": 4, + "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高" + }, + { + "step": 5, + "description": "放入五花肉,保持翻炒至肉变色" + }, + { + "step": 6, + "description": "加入老抽,炒 **1 分钟**,给肉上色" + }, + { + "step": 7, + "description": "加入白菜帮子,加入食用盐、生抽,炒一分钟(如果粘锅,烹入 10ml 水)" + }, + { + "step": 8, + "description": "加水没过所有食材,加入鸡精 ,十三香,沸腾后,将火调小然后**等待 20 分钟**" + }, + { + "step": 9, + "description": "粉条滤水切成小段放入碗中 备用" + }, + { + "step": 10, + "description": "加入白菜嫩叶,炒匀后将粉条放在菜上方,加盖再煮 **5 分钟**" + }, + { + "step": 11, + "description": "尝味、关火,收汁" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-粉蒸肉", + "name": "粉蒸肉的做法", + "description": "# 粉蒸肉的做法\n\n粉蒸肉是一道经典的中式蒸菜,香味浓郁,口感软糯,营养丰富。适合家庭聚餐或节日宴客。此菜适合有一定烹饪经验的人士制作,预计从准备到完成约需 90 分钟。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/粉蒸肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 500g(肥瘦相间)", + "notes": "量未指定" + }, + { + "name": "蒸肉米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸肉米粉 100g(推荐使用李锦记或自制)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 15ml", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣酱 10g(可选)", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末 10g", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末 10g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 5g", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 300g(或南瓜 300g,作为垫底食材)", + "notes": "量未指定" + }, + { + "name": "清水(蒸锅用)2000ml", + "quantity": null, + "unit": null, + "text_quantity": "- 清水(蒸锅用)2000ml", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 250g", + "notes": "量未指定" + }, + { + "name": "蒸肉米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸肉米粉 50g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 7.5ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 7.5ml", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣酱 5g(可选)", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末 5g", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末 5g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 2.5g", + "notes": "量未指定" + }, + { + "name": "土豆或南瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆或南瓜 150g", + "notes": "量未指定" + }, + { + "name": "蒸锅用水", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸锅用水 1000ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将五花肉洗净,切成长约 5cm、宽约 3cm、厚度约 0.5cm 的肉片" + }, + { + "step": 2, + "description": "将姜、蒜切成颗粒直径不大于 1mm 的细末" + }, + { + "step": 3, + "description": "取一大碗,放入切好的五花肉、15ml 生抽、10ml 老抽、15ml 料酒、10g 郫县豆瓣酱、10g 姜末、10g 蒜末、5g 白砂糖" + }, + { + "step": 4, + "description": "用筷子搅拌均匀后,盖上保鲜膜,室温(20°C - 25°C)静置腌制 30 分钟" + }, + { + "step": 5, + "description": "腌制完成后,加入 100g 蒸肉米粉,继续翻拌 2 分钟,确保每片肉都均匀裹粉" + }, + { + "step": 6, + "description": "土豆去皮,切片厚度控制在 0.8cm,片面积约为 5cm x 5cm,重量控制在 300g" + }, + { + "step": 7, + "description": "在直径 20cm 的深碗底部铺满土豆片,尽量无重叠" + }, + { + "step": 8, + "description": "将拌好粉的五花肉均匀铺在土豆片上,压实" + }, + { + "step": 9, + "description": "蒸锅中加入 2000ml 清水,开火加热至水面持续冒泡(100°C)" + }, + { + "step": 10, + "description": "将装好食材的碗放入蒸锅内,盖好锅盖" + }, + { + "step": 11, + "description": "保持中火蒸 60 分钟(火力保持在可持续沸腾的程度,约 600W 热功率)" + }, + { + "step": 12, + "description": "时间结束后,用筷子插入肉块中央,若能轻松穿透并无明显阻力,则表明蒸熟" + }, + { + "step": 13, + "description": "若未达到此状态,则继续加热 10 - 15 分钟,直至肉质软烂,油脂渗出" + }, + { + "step": 14, + "description": "取出盛盘,即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-糖醋里脊", + "name": "糖醋里脊的做法", + "description": "# 糖醋里脊的做法\n\n糖醋里脊是中国经典传统名菜之一,该菜品以猪里脊肉为主材,配以面粉、淀粉、醋等佐料,酸甜可口,让人食欲大开;该菜品在陕菜、豫菜、浙菜、鲁菜、川菜、淮扬菜、粤菜、闽菜里均有此菜。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/糖醋里脊.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "里脊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 里脊肉", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "里脊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 里脊肉 500g", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 10g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 30g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 50g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 50g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 20g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 10g", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 30ml", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉 5g", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "腌肉:将猪里脊肉先切厚片,用刀背拍一拍,把肉拍松一点。切成一个手指头粗的条,加料酒,生抽,蚝油,食盐,白胡椒粉,一个鸡蛋,将肉用手抓匀,腌制 20 分钟以上。" + }, + { + "step": 2, + "description": "调酱:番茄酱+10g 醋+30g 白糖+150ml 清水,搅拌至糖融化,备用。" + }, + { + "step": 3, + "description": "裹粉:先把粉全部裹好再来炸,这样在炸的时候就不会手忙脚乱。准备一个大碗,里面放淀粉,把每一根肉条都满满裹上淀粉。" + }, + { + "step": 4, + "description": "炸制:油温 160 摄氏度下里脊,可以拿一个干筷子放在油里面试一下,周围冒小泡就可以下锅。" + }, + { + "step": 5, + "description": "炸到表面微黄可以捞出,全程中火。然后等油温升高到 200 摄氏度,把里脊倒进去重新炸一次,只需 40 秒,表皮就会很脆,马上捞出。" + }, + { + "step": 6, + "description": "裹酱:另外拿一个锅,锅里放底油,把调好的酱汁倒进去,煮到冒泡,把炸好的里脊放进去,翻炒,让每一根都裹上酱汁。" + }, + { + "step": 7, + "description": "下炸好的里脊肉翻炒,关火盛出。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-肉饼炖蛋", + "name": "肉饼炖蛋的做法", + "description": "# 肉饼炖蛋的做法\n\n肉饼炖蛋是一道传统的中国家常菜,也是一道非常受欢迎的下饭菜。初学者只需要 20 分钟即可完成。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/肉饼炖蛋.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉末", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "芝麻香油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻香油", + "notes": "量未指定" + }, + { + "name": "猪肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉末 300g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 20ml", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉 5g", + "notes": "量未指定" + }, + { + "name": "芝麻香油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻香油 10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "在碗中加入猪肉末、料酒、生抽、白胡椒粉、鸡蛋和芝麻香油,搅拌均匀。" + }, + { + "step": 2, + "description": "将调好味的猪肉末铺在盘子里,肉末中间用勺子挖一个洞,往洞中打入 1 个鸡蛋。" + }, + { + "step": 3, + "description": "锅中加水至 1/4 高度,水烧开后,将盘子放入锅中,盖上锅盖,蒸 15 分钟。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-萝卜炖羊排", + "name": "萝卜炖羊排的做法", + "description": "# 萝卜炖羊排的做法\n\n萝卜炖羊排是一道常见家常菜,老少皆宜。一般初学者只需要最多 2 小时即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/萝卜炖羊排.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "羊排", + "quantity": null, + "unit": null, + "text_quantity": "- 羊排", + "notes": "量未指定" + }, + { + "name": "白萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 白萝卜", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "白芷(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 白芷(可选)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒或者黄酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒或者黄酒", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "羊排", + "quantity": null, + "unit": null, + "text_quantity": "- 羊排 400g", + "notes": "量未指定" + }, + { + "name": "白萝卜一根", + "quantity": null, + "unit": null, + "text_quantity": "- 白萝卜一根", + "notes": "量未指定" + }, + { + "name": "大葱一根", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱一根", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 10 粒", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 10g ,一般买一头姜,从中切大约 4 片即可", + "notes": "量未指定" + }, + { + "name": "料酒或者黄酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒或者黄酒 30ml-40ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 10g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 2-4 粒", + "notes": "量未指定" + }, + { + "name": "水:没过食材的量,需要", + "quantity": null, + "unit": null, + "text_quantity": "- 水:没过食材的量,需要 1000ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "萝卜去皮、滚刀切成 3-5cm 的大块,备用" + }, + { + "step": 2, + "description": "羊排在购买时可以让卖家切好,因为家用刀一般难以切动,备用" + }, + { + "step": 3, + "description": "羊肉冷水下锅,加入一半的料酒,一半的葱姜,煮 10 分钟去掉血腥,(可选)把焯的过程中出现的血沫子可以用勺子盛出来" + }, + { + "step": 4, + "description": "另起一锅冷水,放入切好的白萝卜,放入一半的冰糖,等水开后煮 5 分钟去掉白萝卜的辣味" + }, + { + "step": 5, + "description": "盛出来焯好的羊排,放入高压锅中,加水没过所有食材后再增加大约 300ml 的水" + }, + { + "step": 6, + "description": "将剩余的葱姜料酒,花椒,冰糖,白芷(可选),盐放入锅中,盖锅等待上汽后计时,中火炖大约 15 分钟。" + }, + { + "step": 7, + "description": "关火,等待高压锅放气完毕,开盖,加入之前焯好的萝卜,调味,加入 3-10g 的食盐或者水,品尝汤的咸淡," + }, + { + "step": 8, + "description": "再开火,中火,高压锅上汽再炖 10 分钟,普通锅盖盖再炖 20 分钟" + }, + { + "step": 9, + "description": "关火,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-蒜苔炒肉末", + "name": "蒜苔炒肉末的做法", + "description": "# 蒜苔炒肉末的做法\n\n蒜苔炒肉末是一道简单易做的菜。这是一道北方家常菜,以做法简单、味道上乘的特点,在广大北方人民群众中备受喜爱。\n\n预估烹饪难度:★★", + "source_path": "dishes/meat_dish/蒜苔炒肉末.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 2, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉薄片", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉薄片", + "notes": "量未指定" + }, + { + "name": "蒜苔", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苔", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣", + "notes": "量未指定" + }, + { + "name": "蒜苔", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苔 1 扎(每扎蒜苔约 190g)", + "notes": "量未指定" + }, + { + "name": "五花肉薄片", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉薄片 4 片(约 20g)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10ml", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 2 瓣", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "蒜苔切成 5cm 小段,备用" + }, + { + "step": 2, + "description": "五花肉切成 5mm * 5cm 丝状,备用" + }, + { + "step": 3, + "description": "蒜瓣拍碎切成末,备用" + }, + { + "step": 4, + "description": "热锅,锅内放入 10ml 食用油。等待 10 秒让油温升高" + }, + { + "step": 5, + "description": "放入蒜末,中火翻炒 **10 秒** 将蒜末炒出香味" + }, + { + "step": 6, + "description": "放入五花肉和 5ml 生抽,中火翻炒 **30 秒** 将肉炒熟并上色" + }, + { + "step": 7, + "description": "将蒜苔放入锅内并加入 10ml 生抽,翻炒 **30 秒**" + }, + { + "step": 8, + "description": "锅内加入 20g 水,中火翻炒 **5 分钟** 将蒜苔炒至稍稍变软" + }, + { + "step": 9, + "description": "最后加入 2g 食盐,中火翻炒 **30 秒**,即可出锅装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-虎皮肘子", + "name": "虎皮肘子的做法", + "description": "# 虎皮肘子的做法\n\n虎皮肘子是一道传统名菜,以猪肘为主料,通过先烧再炸后炖三个步骤使肘子皮呈现出虎皮状。肘子皮软烂入味,肥而不腻,瘦肉松软可口。这道菜是逢年过节让老辈子闭嘴猛炫的不二之选,可谓是救命法宝。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/虎皮肘子.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪前肘", + "quantity": null, + "unit": null, + "text_quantity": "- 猪前肘", + "notes": "量未指定" + }, + { + "name": "食用植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用植物油", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "肉桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 肉桂皮", + "notes": "量未指定" + }, + { + "name": "豆蔻", + "quantity": null, + "unit": null, + "text_quantity": "- 豆蔻", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "大料", + "quantity": null, + "unit": null, + "text_quantity": "- 大料", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "1 汤匙 =", + "quantity": null, + "unit": null, + "text_quantity": "- 1 汤匙 = 15ml", + "notes": "量未指定" + }, + { + "name": "1 茶匙 =", + "quantity": null, + "unit": null, + "text_quantity": "- 1 茶匙 = 5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "猪肘解冻后水泡 1 小时去除血水。" + }, + { + "step": 2, + "description": "如有火焰喷枪,则使用火焰喷枪灼烧**猪肘皮**表面至**棕黑色**以去除猪毛,破坏汗腺。注意不要长时间炙烤同一个位置以避免烧焦,当猪肘皮几乎完全呈现棕黑色时则停止灼烧。" + }, + { + "step": 3, + "description": "如无火焰喷枪,将铁锅烧至 200 以上,将猪肘直接放入锅内,用锅铲或筷子使猪肘皮充分接触铁锅表面,当猪肘皮与铁锅接触位置呈现出棕色时,更换位置继续烫猪肘皮,直到整个猪肘被充分烫过。注意再次过程中注意铁锅温度,不要使铁锅红热。" + }, + { + "step": 4, + "description": "使用清洁球在水中刷洗猪肘,将其表面烧焦的部分去除。刷洗结束后,猪肘再次呈现出未被灼烧前的状态。" + }, + { + "step": 5, + "description": "将猪肘置于铁锅中,加尽量多的冷水,具体视铁锅深度与猪肘大小而定,在保证可以拿得动铁锅及其内容物的情况下,能浸没猪肘 3/4 以上为最佳。" + }, + { + "step": 6, + "description": "取 1 棵葱的葱白,分成 3 段,放入锅中。" + }, + { + "step": 7, + "description": "取 3 粒蒜,分别用刀身拍扁,放入锅中。" + }, + { + "step": 8, + "description": "取 3 克姜,放入锅中。" + }, + { + "step": 9, + "description": "将 2 汤匙料酒加入锅中。" + }, + { + "step": 10, + "description": "锅中水烧开后,等待五分钟,随后将猪肘取出,捡出锅中所有配料,更换容器保留所有肉汤备用。" + }, + { + "step": 11, + "description": "向锅中加入冷油,以之前水量为参考,能浸没猪肘 3/5 以上为佳,开中火加热。" + }, + { + "step": 12, + "description": "当[油温](tips/advanced/油温判断技巧.md)达到五成时,转为小火,放入猪肘油炸。" + }, + { + "step": 13, + "description": "在油炸过程中烹饪者应注意人身安全。" + }, + { + "step": 14, + "description": "在油炸过程中,使用锅铲或其他耐高温厨具将锅中的油均匀淋到猪肘未被浸没的部分,如果条件允许应以 3 分钟的间隔翻转猪肘,使其油炸均匀。" + }, + { + "step": 15, + "description": "油炸过程持续大约 20 分钟,当观察到猪肘皮已经全部呈现出浅棕色,而瘦肉部分已经微焦,则可捞出备用。" + }, + { + "step": 16, + "description": "炸制完后的油可用于制作其他油炸类食物,但注意不要使用太多次。" + }, + { + "step": 17, + "description": "[炒糖色](../../tips/advanced/糖色的炒制.md)200ml 备用。" + }, + { + "step": 18, + "description": "将猪肘加入高压锅内,加入所有肉汤、糖色、香叶、肉桂皮、豆蔻、花椒、大料、老抽、生抽、白醋。如果喜欢甜口,可以再额外加入 2-3 克冰糖。" + }, + { + "step": 19, + "description": "取 1 棵葱的葱白,分成 3 段,放入锅中。" + }, + { + "step": 20, + "description": "取 3 粒蒜,分别用刀身拍扁,放入锅中。" + }, + { + "step": 21, + "description": "取 3 克姜,放入锅中。" + }, + { + "step": 22, + "description": "盖上锅盖,加压炖煮 40 分钟。" + }, + { + "step": 23, + "description": "在炖煮期间调制水淀粉。取碗 1 个,加入 1 汤匙淀粉,100ml 水,搅拌使其成为白色悬浊液" + }, + { + "step": 24, + "description": "炖煮时间结束后,打开高压锅锅盖,捡出锅中所有的配料,只保留猪肘。" + }, + { + "step": 25, + "description": "将高压锅中剩余的肉汤转移至铁锅内,猪肘转移至盘子或盆内" + }, + { + "step": 26, + "description": "将铁锅置于灶台上,开大火。在收汁过程中可以用筷子头蘸取锅内汤汁判断咸淡,并根据口味添加盐。注意,汤汁多的时候味道会比汤汁少的时候味道更淡,加入盐时需要考虑这一点。" + }, + { + "step": 27, + "description": "当肉汤沸腾时,注意观察剩余肉汤余量" + }, + { + "step": 28, + "description": "当剩余肉汤少于原肉汤体积的 1/2 时,再次搅拌之前调制好的水淀粉,并加入一半" + }, + { + "step": 29, + "description": "等待肉汤沸腾,加入剩下的一半" + }, + { + "step": 30, + "description": "等待肉汤沸腾,沸腾后等待 1-2 分钟关火,此时锅内的肉汤呈红棕色粘稠状" + }, + { + "step": 31, + "description": "用汤匙舀起肉汤均匀地淋在猪肘上,尽量使猪肘的每一处都淋到汤汁。如果在猪肘被完全淋到前汤汁已经用完则可直接上桌,否则剩余汤汁不需要再淋,可直接上桌。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-蚂蚁上树", + "name": "蚂蚁上树的做法", + "description": "# 蚂蚁上树的做法\n\n蚂蚁上树是一道经典的川菜,主要材料为粉丝和肉末。它咸香微辣、入味透彻,粉丝软滑爽口,肉末细嫩鲜香。全程只需 20 分钟,是非常适合家庭操作的一道菜。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/蚂蚁上树.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "红薯粉丝", + "quantity": null, + "unit": null, + "text_quantity": "- 红薯粉丝", + "notes": "量未指定" + }, + { + "name": "猪肉末(或牛肉末)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉末(或牛肉末)", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣酱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "蒜末、姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末、姜末", + "notes": "量未指定" + }, + { + "name": "小葱(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱(可选)", + "notes": "量未指定" + }, + { + "name": "红薯粉丝", + "quantity": null, + "unit": null, + "text_quantity": "- 红薯粉丝 80g(干重)", + "notes": "量未指定" + }, + { + "name": "猪肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉末 150g", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣酱 15g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5ml", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10ml", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末 10g", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末 5g", + "notes": "量未指定" + }, + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水 300ml(用于煮粉丝)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "红薯粉丝提前泡软,泡水时间为 20 分钟,备用" + }, + { + "step": 2, + "description": "将蒜、姜分别剁碎,备用" + }, + { + "step": 3, + "description": "锅烧热,加入 10ml 食用油,加入蒜末、姜末炒香" + }, + { + "step": 4, + "description": "加入猪肉末翻炒至**肉色发白且微微出油**" + }, + { + "step": 5, + "description": "加入郫县豆瓣酱,炒至**红油析出**" + }, + { + "step": 6, + "description": "加入生抽和老抽,翻炒均匀" + }, + { + "step": 7, + "description": "倒入 300ml 清水,煮沸" + }, + { + "step": 8, + "description": "放入泡软沥干的粉丝,用筷子轻轻拨动防止粘连" + }, + { + "step": 9, + "description": "中小火煮约 5 分钟,直至粉丝**完全吸收汤汁**、呈现微微收干状态" + }, + { + "step": 10, + "description": "依据口味可撒入小葱末,关火装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-辣椒炒肉", + "name": "辣椒炒肉的做法", + "description": "# 辣椒炒肉的做法\n\n⚠️注意:本道菜需要一定料理基础,不推荐新手尝试。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/辣椒炒肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "青椒(吃辣的话推荐用杭椒,螺丝椒,不吃辣的用尖椒,甜椒)", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒(吃辣的话推荐用杭椒,螺丝椒,不吃辣的用尖椒,甜椒)", + "notes": "量未指定" + }, + { + "name": "猪瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪瘦肉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "酱油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油(可选)", + "notes": "量未指定" + }, + { + "name": "豆豉(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉(可选)", + "notes": "量未指定" + }, + { + "name": "青椒的数量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒的数量 = 份数 * 3 个。", + "notes": "量未指定" + }, + { + "name": "肉量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 肉量 = 份数 * 200g。", + "notes": "量未指定" + }, + { + "name": "盐量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 份数 * 3g。", + "notes": "量未指定" + }, + { + "name": "生抽 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 = 份数 * 3ml。", + "notes": "量未指定" + }, + { + "name": "蚝油 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 = 份数 * 3ml。", + "notes": "量未指定" + }, + { + "name": "大蒜 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 = 份数 * 5g。", + "notes": "量未指定" + }, + { + "name": "生姜 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 = 份数 * 5g。", + "notes": "量未指定" + }, + { + "name": "酱油 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 = 份数 * 2ml。", + "notes": "量未指定" + }, + { + "name": "豆豉 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉 = 份数 * 3g。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将`青椒`洗净,去除`青椒把`以及`青椒籽`,再用`滚刀手法`切好备用。" + }, + { + "step": 2, + "description": "`大蒜`用刀拍一下,再横切成`蒜瓣`,`生姜`切碎成`姜末`。" + }, + { + "step": 3, + "description": "将`猪瘦肉`切成`肉片`(顺着猪肉的纹理切,即刀和肉的纹理呈水平线,出来的肉片,纹路呈“川”字)。" + }, + { + "step": 4, + "description": "将切好的`猪肉`洗净,放入空碗,再加入计算好的`生抽`、`蚝油`、`盐`搅拌均匀,腌制 10 分钟。" + }, + { + "step": 5, + "description": "热锅,不用倒油,把`切好的青椒`放入锅中,大火干煸至虎皮状后,再加 2g`盐`继续翻炒 1 分钟 后捞起。" + }, + { + "step": 6, + "description": "不用洗锅,大火热锅,加入份数 * 8ml`油`,等待 30s,加入`蒜瓣`、`姜末`翻炒 15s。" + }, + { + "step": 7, + "description": "加入腌制好的`猪肉`倒入锅内翻炒 2 分钟,再加入干煸过的`青椒`翻炒 1 分钟。" + }, + { + "step": 8, + "description": "根据个人口味喜好加入`豆豉`,最后加入`酱油`,继续翻炒 30s。" + }, + { + "step": 9, + "description": "出锅,盛盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-香干肉丝", + "name": "香干肉丝的做法", + "description": "# 香干肉丝的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/香干肉丝.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪里脊(可以买超市切好且称重好的肉丝)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪里脊(可以买超市切好且称重好的肉丝)", + "notes": "量未指定" + }, + { + "name": "香干", + "quantity": null, + "unit": null, + "text_quantity": "- 香干", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "香干 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 香干 = 份数 * 75g", + "notes": "量未指定" + }, + { + "name": "青椒的数量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒的数量 = 份数 * 5 个", + "notes": "量未指定" + }, + { + "name": "肉量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 肉量 = 份数 * 100g", + "notes": "量未指定" + }, + { + "name": "盐量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 份数 * 3g", + "notes": "量未指定" + }, + { + "name": "生抽 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 = 份数 * 5ml", + "notes": "量未指定" + }, + { + "name": "淀粉 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 = 份数 * 5g", + "notes": "量未指定" + }, + { + "name": "大蒜 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 = 份数 * 5g(大约 3 个蒜瓣)", + "notes": "量未指定" + }, + { + "name": "鸡精 =", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 = 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "`肉丝`(没有肉丝,自己切)用生抽(3ml),生粉混合均匀待用。" + }, + { + "step": 2, + "description": "将`青椒`洗净,再用`滚刀手法`切好备用。" + }, + { + "step": 3, + "description": "`大蒜`横切成片,`香干`切丝。" + }, + { + "step": 4, + "description": "`淀粉`与水(10ml)混合,搅拌均匀。" + }, + { + "step": 5, + "description": "干净锅 15ml 油,不用等油热就倒入肉丝慢慢划散,肉丝熟了,立马捞出,留油到锅里。" + }, + { + "step": 6, + "description": "将蒜片和香干放入锅中,加入 2ml 生抽,翻炒均匀。" + }, + { + "step": 7, + "description": "2-3 分钟,看火大小,将青椒丝放入锅中混合,翻炒。" + }, + { + "step": 8, + "description": "1 分钟后,放入肉丝混合。" + }, + { + "step": 9, + "description": "倒入淀粉与水的混合物勾芡,加入盐 3g,鸡精 2g,翻炒 2-3 分钟出锅。" + }, + { + "step": 10, + "description": "成品。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-鱼香肉丝", + "name": "鱼香肉丝的做法", + "description": "# 鱼香肉丝的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/鱼香肉丝.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "里脊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 里脊肉 200g", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 100g", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 100g", + "notes": "量未指定" + }, + { + "name": "木耳(干)", + "quantity": null, + "unit": null, + "text_quantity": "- 木耳(干) 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 5ml", + "notes": "量未指定" + }, + { + "name": "蛋清", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋清 1 个", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 10g", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 15ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 10g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 20g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 20g", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2 瓣", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 15g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "制作`腌料`:将下列原料混合:" + }, + { + "step": 2, + "description": "生抽 5ml" + }, + { + "step": 3, + "description": "料酒 5ml" + }, + { + "step": 4, + "description": "淀粉 5g" + }, + { + "step": 5, + "description": "水 20ml" + }, + { + "step": 6, + "description": "蛋清 1 个" + }, + { + "step": 7, + "description": "制作`香汁`:将下列原料混合:" + }, + { + "step": 8, + "description": "生抽 5ml" + }, + { + "step": 9, + "description": "醋 15ml" + }, + { + "step": 10, + "description": "白糖 10 克" + }, + { + "step": 11, + "description": "盐 1 克" + }, + { + "step": 12, + "description": "淀粉 5g" + }, + { + "step": 13, + "description": "水 20ml" + }, + { + "step": 14, + "description": "用`腌料`腌制里脊肉 15-30 分钟。注意将肉抓匀。" + }, + { + "step": 15, + "description": "干木耳泡 4 个小时,洗净,切成小块。" + }, + { + "step": 16, + "description": "青椒洗净,去蒂,切成丝。" + }, + { + "step": 17, + "description": "胡萝卜洗净,切成丝,将胡萝卜丝[焯水](../../tips/learn/学习焯水.md)。" + }, + { + "step": 18, + "description": "姜、蒜切沫。" + }, + { + "step": 19, + "description": "葱切成 5mm 的小段。" + }, + { + "step": 20, + "description": "将锅烧热,加入 15ml 油。" + }, + { + "step": 21, + "description": "向锅内倒入准备好的腌肉,快速滑散至变白,盛出备用。" + }, + { + "step": 22, + "description": "将锅烧热,加入 5ml 油。" + }, + { + "step": 23, + "description": "倒入全部`葱`、`姜`、`蒜`、`豆瓣酱`。" + }, + { + "step": 24, + "description": "倒入全部`胡萝卜`,翻炒 20s 后,放入青椒和木耳,翻炒 2 分钟。" + }, + { + "step": 25, + "description": "倒入`炒过的肉`。快速翻炒均匀。注意不要炒超过 20 秒。" + }, + { + "step": 26, + "description": "倒入`香汁`。快速翻炒均匀。注意不要炒超过 15 秒。" + }, + { + "step": 27, + "description": "关火,盛盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-麻辣香锅", + "name": "麻辣香锅的做法", + "description": "# 麻辣香锅的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/麻辣香锅.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "青菜(油菜、油麦菜、菠菜)", + "quantity": null, + "unit": null, + "text_quantity": "- 青菜(油菜、油麦菜、菠菜)", + "notes": "量未指定" + }, + { + "name": "无骨肉(猪肉、牛肉、鸡肉、鱼丸、火腿肠)", + "quantity": null, + "unit": null, + "text_quantity": "- 无骨肉(猪肉、牛肉、鸡肉、鱼丸、火腿肠)", + "notes": "量未指定" + }, + { + "name": "干豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 干豆腐", + "notes": "量未指定" + }, + { + "name": "北京麻辣方便面", + "quantity": null, + "unit": null, + "text_quantity": "- 北京麻辣方便面", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "青菜共需", + "quantity": null, + "unit": null, + "text_quantity": "- 青菜共需 455 克,其中油菜、油麦菜、菠菜的比例按自己喜好分配即可", + "notes": "量未指定" + }, + { + "name": "无骨肉共需", + "quantity": null, + "unit": null, + "text_quantity": "- 无骨肉共需 430 克,其中猪肉、牛肉、鸡肉、鱼丸、火腿肠的比例按自己喜好分配即可", + "notes": "量未指定" + }, + { + "name": "干豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 干豆腐 152 克", + "notes": "量未指定" + }, + { + "name": "北京麻辣方便面", + "quantity": null, + "unit": null, + "text_quantity": "- 北京麻辣方便面 1 袋", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 5 克", + "notes": "量未指定" + }, + { + "name": "麻辣香锅调料", + "quantity": null, + "unit": null, + "text_quantity": "- 麻辣香锅调料 110 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-黄焖鸡", + "name": "黄焖鸡的做法", + "description": "# 黄焖鸡的做法\n\n黄焖鸡是一道十分下饭的美食,食材平平无奇又十分容易烹制,一学就会。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/黄焖鸡.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡腿", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿", + "notes": "量未指定" + }, + { + "name": "香菇(干香菇最佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 香菇(干香菇最佳)", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "生姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜片", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "土豆(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆(可选)", + "notes": "量未指定" + }, + { + "name": "鸡腿 = 两只", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿 = 两只", + "notes": "量未指定" + }, + { + "name": "香菇(干香菇最佳)=", + "quantity": null, + "unit": null, + "text_quantity": "- 香菇(干香菇最佳)= 5 朵", + "notes": "量未指定" + }, + { + "name": "青椒 = 两个", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 = 两个", + "notes": "量未指定" + }, + { + "name": "生姜片 = 两片", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜片 = 两片", + "notes": "量未指定" + }, + { + "name": "干辣椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 = 5,6 个", + "notes": "量未指定" + }, + { + "name": "盐 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = 10g", + "notes": "量未指定" + }, + { + "name": "料酒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 = 10ml", + "notes": "量未指定" + }, + { + "name": "白胡椒粉 =", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉 = 5g", + "notes": "量未指定" + }, + { + "name": "白糖 =", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 = 5g", + "notes": "量未指定" + }, + { + "name": "酱油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 = 5ml", + "notes": "量未指定" + }, + { + "name": "土豆 = 一个(可选,可使汤汁更粘稠)", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 = 一个(可选,可使汤汁更粘稠)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡腿洗净,剁成**4cm**大小的块" + }, + { + "step": 2, + "description": "生姜切片、干辣椒切成**小圈**" + }, + { + "step": 3, + "description": "香菇切片,青椒切成细长的**马蹄状**,若为干香菇,洗净灰尘后泡一晚上并留香菇水备用" + }, + { + "step": 4, + "description": "若有土豆,切为与鸡肉大小类似的**滚刀块**" + }, + { + "step": 5, + "description": "炒糖色:锅里倒入底油,冷油时放入白糖(**有一定难度,新手可跳至鸡肉炒制并使用老抽替代**)" + }, + { + "step": 6, + "description": "小火慢慢加热,待油温逐渐升高,白糖开始融化并变成较深的棕色(期间要不断搅拌,防止糊锅)" + }, + { + "step": 7, + "description": "迅速倒入鸡块,转大火,快速翻炒!烹入料酒,继续翻炒片刻" + }, + { + "step": 8, + "description": "将生姜片和干辣椒倒入" + }, + { + "step": 9, + "description": "放入酱油,炒匀" + }, + { + "step": 10, + "description": "倒入香菇水或清水,以能淹住鸡肉为准" + }, + { + "step": 11, + "description": "倒入香菇片,白胡椒粉,盐,土豆" + }, + { + "step": 12, + "description": "翻炒均匀后,盖上锅盖焖煮,转中小火**15——20分钟**,有条件可以转至砂锅" + }, + { + "step": 13, + "description": "待鸡肉软烂,汤汁浓稠后(汤汁不要收的太干),最后放入青椒" + }, + { + "step": 14, + "description": "放入味精,兜炒均匀后,关火!青椒基本断生即可,不要炒时间久了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-黄瓜炒肉", + "name": "黄瓜炒肉的做法", + "description": "# 黄瓜炒肉的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/黄瓜炒肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜", + "notes": "量未指定" + }, + { + "name": "猪瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪瘦肉", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "黄瓜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜 = 100 克 * 份数", + "notes": "量未指定" + }, + { + "name": "猪肉 =", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉 = 50 克 * 份数", + "notes": "量未指定" + }, + { + "name": "油量 =", + "quantity": null, + "unit": null, + "text_quantity": "- 油量 = 50 克 * 份数", + "notes": "量未指定" + }, + { + "name": "盐量 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 10 克 * 份数", + "notes": "量未指定" + }, + { + "name": "酱油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 = 5 克 * 份数", + "notes": "量未指定" + }, + { + "name": "蒜瓣 =", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 = 2 瓣 * 份数", + "notes": "量未指定" + }, + { + "name": "小米辣 =", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 = 1 根 * 份数", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将猪瘦肉切片,放入碗中,倒入食用油 10 克,生抽,搅拌均匀,腌制 10 分钟" + }, + { + "step": 2, + "description": "将黄瓜切去 5 厘米的头尾,剩余部分斜着切成 0.5 厘米的薄片" + }, + { + "step": 3, + "description": "将黄瓜倒入碗中,撒上盐 8 克,搅拌均匀,腌制 5 分钟" + }, + { + "step": 4, + "description": "将蒜瓣去皮,压扁,切成蒜末备用" + }, + { + "step": 5, + "description": "将小米辣去丁切分成均匀 0.5 厘米的段状" + }, + { + "step": 6, + "description": "热锅,倒油 40 克,等油温到冒烟,放入蒜蓉小米辣翻炒 5 次" + }, + { + "step": 7, + "description": "放入腌制好的猪瘦肉,翻炒至肉熟变色" + }, + { + "step": 8, + "description": "放入黄瓜,加入盐 2 克,大火翻炒均匀半分钟,出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-农家一碗香-农家一碗香", + "name": "农家一碗香的做法", + "description": "# 农家一碗香的做法\n\n![农家一碗香](./农家一碗香成品.jpg)\n\n农家一碗香,是一道地道的湖南菜,里面主要食材有青椒、鸡蛋和猪肉。味道咸香下饭,而且这道菜烹饪简单,不需要特别的处理。\n\n农家一碗香是一道中等难度的菜品。预计备菜 7 分钟,烹饪 10 分钟,总计 17 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/农家一碗香/农家一碗香.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/农家一碗香/农家一碗香成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/农家一碗香/农家一碗香成品.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-冬瓜酿肉-冬瓜酿肉", + "name": "冬瓜酿肉的做法", + "description": "# 冬瓜酿肉的做法\n\n![冬瓜酿肉成品](./冬瓜酿肉成品.jpg)\n\n荤素搭配,鲜嫩爽滑,做法简单。一般 30 分钟。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/冬瓜酿肉/冬瓜酿肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/冬瓜形状.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/冬瓜形状.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/冬瓜酿肉成品.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/卷肉.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/打鸡蛋.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/摆盘.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/腌制好的冬瓜.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "冬瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 冬瓜", + "notes": "量未指定" + }, + { + "name": "猪肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉末", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "葱姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜末", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "猪肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉末 300g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个(可选,不习惯的人可能会有点腥)", + "notes": "量未指定" + }, + { + "name": "冬瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 冬瓜 200g", + "notes": "量未指定" + }, + { + "name": "葱花(一根,约", + "quantity": null, + "unit": null, + "text_quantity": "- 葱花(一根,约 20g)", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 5g", + "notes": "量未指定" + }, + { + "name": "水淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 水淀粉 25g(淀粉 25g,水 50ml)", + "notes": "量未指定" + }, + { + "name": "葱姜末(姜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜末(姜 3-4 片约 30g, 取上面一根葱花中的葱白部分即可)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "冬瓜去皮,切成 25cm 长 3cm 厚的片" + }, + { + "step": 2, + "description": "将切好的冬瓜放入碗中,放入 15g 盐,将冬瓜抹匀,放置 10 分钟" + }, + { + "step": 3, + "description": "放置冬瓜的同时,换个碗放入肉末,葱姜末, 5g 盐,淀粉 5g,胡椒粉,生抽,胡椒粉" + }, + { + "step": 4, + "description": "使用筷子在肉末中进行顺时针搅拌,搅拌到食材颜色没有明显对比(约 2 分钟)" + }, + { + "step": 5, + "description": "将腌制好的冬瓜(会变软)使用清水洗三遍" + }, + { + "step": 6, + "description": "拿出 1 片冬瓜片卷起来,并把肉塞进去" + }, + { + "step": 7, + "description": "放入碟子中摆到碟子的边缘" + }, + { + "step": 8, + "description": "打入 1 个鸡蛋到中间圆圈处" + }, + { + "step": 9, + "description": "放入普通铁锅中水烧开后,蒸 15 分钟,盖上锅盖" + }, + { + "step": 10, + "description": "开盖,取出蒸好的冬瓜酿肉" + }, + { + "step": 11, + "description": "将冬瓜酿肉碟子的水倒入锅中,放入水淀粉,加入 50ml 清水倒入锅中烧开" + }, + { + "step": 12, + "description": "淋到冬瓜酿肉上" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-凉拌鸡丝-凉拌鸡丝", + "name": "凉拌鸡丝的做法", + "description": "# 凉拌鸡丝的做法\n\n![凉拌鸡丝成品](./凉拌鸡丝.jpg)\n隔离期间的一道快手菜,少油低卡,制作简单,预计制作时间 30 分钟\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/凉拌鸡丝/凉拌鸡丝.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝_撕.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝_焯水.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡胸肉(常温冷冻均可)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡胸肉(常温冷冻均可)", + "notes": "量未指定" + }, + { + "name": "麻油(花椒油)", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油(花椒油)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "凉白开水", + "quantity": null, + "unit": null, + "text_quantity": "- 凉白开水", + "notes": "量未指定" + }, + { + "name": "鸡胸肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡胸肉 200 克", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油 5 毫升", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 4 毫升", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 4 毫升", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 3 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 克", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 20 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "姜切片,备用" + }, + { + "step": 2, + "description": "锅中倒入 4 升水" + }, + { + "step": 3, + "description": "加入鸡胸肉、姜片" + }, + { + "step": 4, + "description": "倒入 20 毫升料酒" + }, + { + "step": 5, + "description": "开大火不盖盖将水烧开" + }, + { + "step": 6, + "description": "水开后转中火,用勺子将浮沫捞出" + }, + { + "step": 7, + "description": "继续煮 **5-7** 分钟,如果是非冷冻肉煮 5 分钟,冷冻肉煮 7 分钟" + }, + { + "step": 8, + "description": "鸡胸肉大小会影响成熟时间,用筷子插入鸡胸肉,如果能轻松插入,代表鸡肉熟了。如果不熟需延长煮制时间" + }, + { + "step": 9, + "description": "用凉白开水冲泡鸡胸肉,使鸡胸肉降至室温" + }, + { + "step": 10, + "description": "顺着鸡胸肉纹理将鸡胸肉撕成细丝" + }, + { + "step": 11, + "description": "准备一个碗" + }, + { + "step": 12, + "description": "碗中加入准备好的麻油、生抽、香醋、白糖、盐" + }, + { + "step": 13, + "description": "搅拌料汁,使糖和盐尽量溶化" + }, + { + "step": 14, + "description": "将料汁倒入鸡丝中,搅拌均匀" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-卤菜-卤菜", + "name": "卤菜的做法", + "description": "# 卤菜的做法\n\n卤菜是一道经典的中式卤味料理,富含蛋白质和多种维生素。肉质鲜嫩多汁,香气四溢,入味程度可根据浸泡时间自行调整。这道菜适合作为凉菜、下酒菜或搭配主食食用,卤水还可多次使用,越陈越香。\n本教程以卤牛肉为例,其他肉类同理。\n\n![卤牛肉](./卤牛肉.jpeg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/卤菜/卤菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/卤水.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/卤水.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/卤牛肉.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/牛肉面.jpeg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "卤料包(超市即可购买)", + "quantity": null, + "unit": null, + "text_quantity": "- 卤料包(超市即可购买)", + "notes": "量未指定" + }, + { + "name": "黄豆酱", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "南腐乳", + "quantity": null, + "unit": null, + "text_quantity": "- 南腐乳", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "白糖(最好是黄冰糖,用于熬糖色)", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖(最好是黄冰糖,用于熬糖色)", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒", + "notes": "量未指定" + }, + { + "name": "牛腱子(或其他肉类)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腱子(或其他肉类)", + "notes": "量未指定" + }, + { + "name": "高压锅", + "quantity": null, + "unit": null, + "text_quantity": "- 高压锅", + "notes": "量未指定" + }, + { + "name": "滤网", + "quantity": null, + "unit": null, + "text_quantity": "- 滤网", + "notes": "量未指定" + }, + { + "name": "卤料包", + "quantity": null, + "unit": null, + "text_quantity": "- 卤料包 1 包(约 10g)", + "notes": "量未指定" + }, + { + "name": "黄豆酱", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱 15ml", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 15ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 15ml", + "notes": "量未指定" + }, + { + "name": "南腐乳", + "quantity": null, + "unit": null, + "text_quantity": "- 南腐乳 15ml", + "notes": "量未指定" + }, + { + "name": "洋葱 半个(约", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 半个(约 100g)", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 30g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 40g", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 10g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 120ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 60ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 10-15g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 30g(用于熬糖色)", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒 1 罐(330ml)", + "notes": "量未指定" + }, + { + "name": "牛腱子", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腱子 500g", + "notes": "量未指定" + }, + { + "name": "清水 足量(需要没过所有肉类)", + "quantity": null, + "unit": null, + "text_quantity": "- 清水 足量(需要没过所有肉类)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "牛腱子提前浸泡在冷水中 3 小时以上,去除血水" + }, + { + "step": 2, + "description": "准备糖色:锅中加入 30g 白糖,小火加热至糖完全融化并呈现棕褐色,加入 150ml 热水,搅拌均匀备用" + }, + { + "step": 3, + "description": "将洋葱切块,生姜和大蒜拍碎,干辣椒掰断备用" + }, + { + "step": 4, + "description": "在锅中加入足量的水,放入卤料包、洋葱、生姜、大蒜、干辣椒,大火烧开" + }, + { + "step": 5, + "description": "加入黄豆酱、豆瓣酱、蚝油和南腐乳各 15ml,搅拌均匀" + }, + { + "step": 6, + "description": "倒入准备好的糖色,混合均匀" + }, + { + "step": 7, + "description": "加入生抽 120ml 和老抽 60ml,搅拌均匀" + }, + { + "step": 8, + "description": "加入 10-15g 盐调味" + }, + { + "step": 9, + "description": "倒入 1 罐啤酒(330ml),再次烧开" + }, + { + "step": 10, + "description": "牛腱子放入锅中焯水 2-3 分钟,捞出并用热水冲洗干净表面的浮沫" + }, + { + "step": 11, + "description": "将焯水后的牛腱子放入已烧开的卤水中,确保卤水没过所有肉类" + }, + { + "step": 12, + "description": "盖上高压锅盖,上汽后继续烹饪 25-30 分钟" + }, + { + "step": 13, + "description": "烹饪完成后,不要开盖保温,自然冷却并浸泡一晚上(这样会更入味)" + }, + { + "step": 14, + "description": "将卤好的肉取出放入冰箱冷藏,使其成型" + }, + { + "step": 15, + "description": "食用前取出切片,可直接食用或凉拌" + }, + { + "step": 16, + "description": "卤水重复使用方法:每次卤完肉后,将卤水过滤,去除所有固体内容物,重新烧开杀菌,冷却后可冷藏或冷冻保存。使用时按原配方比例重新添加调味料。(加水量根据卤水使用情况而定)" + }, + { + "step": 17, + "description": "卤水保存得当可以使用很长时间,且越老越香。" + }, + { + "step": 18, + "description": "**重要提示**:如卤制素菜,必须另取一部分卤水,单独使用,并且卤制完素菜的卤水不可重复使用。" + }, + { + "step": 19, + "description": "将蒜末、葱花、白芝麻、辣椒粉按 1:1:1:1 的比例混合,依个人口味加小米辣,热植物油中加入少量芝麻油或藤椒油,分次泼在调料上,再加入生抽、醋、蚝油各 10ml,5ml 糖,味精/鸡精,最后 15ml 卤汤混合均匀。" + }, + { + "step": 20, + "description": "凉拌时可搭配拍黄瓜、木耳、油炸花生米、香菜等配菜。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-口水鸡-口水鸡", + "name": "口水鸡的做法", + "description": "# 口水鸡的做法\n\n![口水鸡](./口水鸡.jpg)\n\n口水鸡(凉菜),炎炎夏日里,热菜难以入口,但又嗜肉如命,\n除了口水鸡,实在想不出更好的适合在夏天吃的肉菜了。\n被红油包裹的鸡肉,红艳鲜亮,冰爽 Q 弹,鲜美而不腻。夏日米饭杀手当之无愧!\n(注:口水鸡做法多样,欢迎补充)\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/口水鸡/口水鸡.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/口水鸡/口水鸡.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/口水鸡/口水鸡.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "半只鸡", + "quantity": null, + "unit": null, + "text_quantity": "- 半只鸡", + "notes": "量未指定" + }, + { + "name": "辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒粉", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "花生", + "quantity": null, + "unit": null, + "text_quantity": "- 花生", + "notes": "量未指定" + }, + { + "name": "葱姜蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱姜蒜", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 20ml", + "notes": "量未指定" + }, + { + "name": "鸡 半只(500g)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡 半只(500g)", + "notes": "量未指定" + }, + { + "name": "辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒粉 20g", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 30 颗(20g)", + "notes": "量未指定" + }, + { + "name": "花生", + "quantity": null, + "unit": null, + "text_quantity": "- 花生 10 颗(30g)", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 2 颗(50g)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 1 小块(20g)", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2 个 (10g)", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 5ml", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精 5g", + "notes": "量未指定" + }, + { + "name": "花椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒粉 5g", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "姜切片,1 颗小葱,15 颗花椒备用" + }, + { + "step": 2, + "description": "鸡肉洗干净,放入锅中,清水没过鸡肉,放入姜片、小葱和花椒,开大火烧开。" + }, + { + "step": 3, + "description": "大火烧开后,转中小火 20 分钟关火" + }, + { + "step": 4, + "description": "取出鸡肉,放入冰水中,直至冰凉" + }, + { + "step": 5, + "description": "取出鸡肉,切块摆盘子中,备用" + }, + { + "step": 6, + "description": "小火把锅烧热,导入花生,烘烤至表皮爆裂。(注意随时翻动,不要糊了)" + }, + { + "step": 7, + "description": "一颗葱切成段,蒜拍成末,花椒 15 颗,花生去皮切碎。" + }, + { + "step": 8, + "description": "锅内导入油烧热后,放入葱段,花椒和一半蒜末,炒香" + }, + { + "step": 9, + "description": "炒至油温 8 成热,关火,滤出热油" + }, + { + "step": 10, + "description": "将热油倒入放辣椒粉的碗中,搅拌,并滤出红油" + }, + { + "step": 11, + "description": "红油中放入剩余蒜末、生抽、醋、盐、味精、糖、香油、花椒粉。拌匀放凉" + }, + { + "step": 12, + "description": "在鸡肉上撒上花生碎,把红油淋到切好的鸡肉上,撒上香菜。成盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-台式卤肉饭-台式卤肉饭", + "name": "台式卤肉饭的做法", + "description": "# 台式卤肉饭的做法\n\n糖和脂肪是人类快乐的源泉,富含这二者的台式卤肉饭每一口都能带来直击灵魂的满足感。\n\n本文提供一种操作简单但风味不减的台式卤肉饭做法,预计制作时间 1.5 小时(0.5 小时操作,1 小时炖煮)。\n\n厨房小白可上手。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/台式卤肉饭/台式卤肉饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/台式卤肉饭/1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/台式卤肉饭/1.jpg" + ], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "红葱头(火葱)", + "quantity": null, + "unit": null, + "text_quantity": "- 红葱头(火葱)", + "notes": "量未指定" + }, + { + "name": "带皮五花肉 (可用猪绞肉代替)", + "quantity": null, + "unit": null, + "text_quantity": "- 带皮五花肉 (可用猪绞肉代替)", + "notes": "量未指定" + }, + { + "name": "鸡蛋(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋(可选)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油", + "notes": "量未指定" + }, + { + "name": "米酒(可用料酒代替)", + "quantity": null, + "unit": null, + "text_quantity": "- 米酒(可用料酒代替)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "五香粉(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉(可选)", + "notes": "量未指定" + }, + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭", + "notes": "量未指定" + }, + { + "name": "红葱头", + "quantity": null, + "unit": null, + "text_quantity": "- 红葱头 25 g", + "notes": "量未指定" + }, + { + "name": "带皮五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 带皮五花肉 500g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 4 个(可任意修改鸡蛋个数)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15 ml", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油 75 ml", + "notes": "量未指定" + }, + { + "name": "米酒", + "quantity": null, + "unit": null, + "text_quantity": "- 米酒 10 + 25 ml", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 25 g", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 2 片", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 1 颗", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 20 g", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉 6 g", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 6 g", + "notes": "量未指定" + }, + { + "name": "米饭 (根据个人食量决定)", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭 (根据个人食量决定)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "带皮五花肉切成 $0.7cm(长)\\times 0.7cm(宽) \\times 2.5cm(高)$ 的细长条" + }, + { + "step": 2, + "description": "红葱头、大蒜切末备用" + }, + { + "step": 3, + "description": "鸡蛋煮熟剥壳,并用刀划破蛋白(便于入味),备用。" + }, + { + "step": 4, + "description": "**大火**热锅,锅内放入 15 ml 食用油,让油滑满锅底即可。" + }, + { + "step": 5, + "description": "放入五花肉条,翻炒至肉色稍微变白,沿锅边淋入米酒 10ml 。继续翻炒至五花肉不再出油。" + }, + { + "step": 6, + "description": "将切好的红葱头加入锅中,翻炒 1 分钟爆出油葱香味。" + }, + { + "step": 7, + "description": "将切好的红葱头加入锅中,翻炒 30 秒。" + }, + { + "step": 8, + "description": "把猪肉推到旁边,放入冰糖加热到融化冒泡变成焦糖,再把猪肉一起翻拌,让焦糖均匀附着。" + }, + { + "step": 9, + "description": "加入生抽炒出香气。" + }, + { + "step": 10, + "description": "呛入米酒 25 ml ,水加到淹过猪肉,加入白胡椒粉、五香粉、八角、香叶、水煮蛋,沸腾后转小火卤 1 小时。" + }, + { + "step": 11, + "description": "1 小时后,开大火收汁直到酱汁浓稠,呈现有光泽的琥珀色,即完成。" + }, + { + "step": 12, + "description": "炖煮结束后,乘一碗米饭,将软烂的卤肉浇在米饭上,并加上卤蛋,开始享用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-咖喱肥牛-咖喱肥牛", + "name": "咖喱肥牛的做法", + "description": "# 咖喱肥牛的做法\n\n![咖喱肥牛成品](./咖喱肥牛.jpg)\n\n咖喱肥牛美味营养并且下饭,吃多了炒炸菜后再吃个咖喱肥牛相当美滋滋。\n\n适合在家吃或者做成便当带去公司吃(微波炉加热也不会有太大味道~)。\n\n并且所需材料少,容易购买,新手一般 40 分钟即可出锅。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/咖喱肥牛/咖喱肥牛.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/咖喱肥牛/咖喱肥牛.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/咖喱肥牛/咖喱肥牛.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "纯牛奶(推荐卫岗鲜奶)", + "quantity": null, + "unit": null, + "text_quantity": "- 纯牛奶(推荐卫岗鲜奶)", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "肥牛卷", + "quantity": null, + "unit": null, + "text_quantity": "- 肥牛卷", + "notes": "量未指定" + }, + { + "name": "咖喱块", + "quantity": null, + "unit": null, + "text_quantity": "- 咖喱块", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 1 片", + "notes": "量未指定" + }, + { + "name": "纯牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 纯牛奶 50ml", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 100g", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 150g", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 200g (挺饱腹的,酌情添加)", + "notes": "量未指定" + }, + { + "name": "肥牛卷", + "quantity": null, + "unit": null, + "text_quantity": "- 肥牛卷 300g (喜欢吃肉就多来点)", + "notes": "量未指定" + }, + { + "name": "咖喱块", + "quantity": null, + "unit": null, + "text_quantity": "- 咖喱块 2 块 (大约 100g)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "洋葱切成条状、胡萝卜以及土豆切成块状,备用" + }, + { + "step": 2, + "description": "烧一锅开水,水沸时将肥牛卷下锅,捞出血沫后放在一边沥水,备用" + }, + { + "step": 3, + "description": "热锅,锅内放入 10ml - 15ml 食用油,**等待 10 秒让油温升高**" + }, + { + "step": 4, + "description": "放入洋葱,翻炒至洋葱变软变透明" + }, + { + "step": 5, + "description": "放入土豆以及胡萝卜**翻炒 2 分钟**" + }, + { + "step": 6, + "description": "加入冷水至淹没所有食材即可" + }, + { + "step": 7, + "description": "将香叶、咖喱块投入锅中,盖上锅盖,**待水沸腾后将火调小然后等待直至土豆块以及胡萝卜块炖至软烂(可用筷子确认)**" + }, + { + "step": 8, + "description": "加入肥牛卷以及牛奶,盖上锅盖再小火煮 2-3 分钟即可出锅(用勺子搅拌食材,注意力度,避免肥牛卷破碎)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-啤酒鸭-啤酒鸭", + "name": "啤酒鸭的做法", + "description": "# 啤酒鸭的做法\n\n![啤酒鸭成品](./啤酒鸭.jpg)\n\n啤酒鸭不仅入口鲜香,还带有一股啤酒清香。肉久吃不腻,汤久涮而不淡。风味独特,具有热而不浮,香而不腻的独特口味让人赞口不绝。一般初学者需要 1 小时即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/啤酒鸭/啤酒鸭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/啤酒鸭/啤酒鸭.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/啤酒鸭/啤酒鸭.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸭肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鸭肉", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油", + "notes": "量未指定" + }, + { + "name": "老抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽酱油", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "丁香", + "quantity": null, + "unit": null, + "text_quantity": "- 丁香", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "鸭肉半只(约", + "quantity": null, + "unit": null, + "text_quantity": "- 鸭肉半只(约 1kg)", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒 800ml", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油 10-15ml", + "notes": "量未指定" + }, + { + "name": "老抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽酱油 5-10ml", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 5 片", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 12 瓣", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 10g", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 5 个", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 30ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 8g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 5g", + "notes": "量未指定" + }, + { + "name": "丁香", + "quantity": null, + "unit": null, + "text_quantity": "- 丁香 4 颗", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 3 个", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 3 片", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "把鸭子切成 3 cm 小块,鸭肉冷水下锅,加姜片、料酒,焯一遍水,盛出沥干水分,备用。" + }, + { + "step": 2, + "description": "炒锅烧热,放入约 100ml 食用油,大火待油烧开,鸭肉入锅翻炒至上色。" + }, + { + "step": 3, + "description": "待鸭肉完全变色(肉眼可见泛白),将鸭肉拨到锅的一边,倒入豆瓣酱和糖,小火翻炒出香味和糖色。" + }, + { + "step": 4, + "description": "加入丁香、八角、香叶、干辣椒、生抽、老抽、蒜,翻炒出香味。" + }, + { + "step": 5, + "description": "倒入啤酒,没过鸭肉,加入盐、鸡精,然后中火将鸭子烧 30 分钟(牙口不好的话可以再多烧 5 分钟)。" + }, + { + "step": 6, + "description": "出锅盛盘,上桌食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-回锅肉-回锅肉", + "name": "回锅肉的做法", + "description": "# 回锅肉的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/回锅肉/回锅肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/回锅肉/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/回锅肉/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/回锅肉/2.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "青红椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青红椒", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 2 棵", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 10-40g", + "notes": "量未指定" + }, + { + "name": "青红椒(根据受辣程度选择,", + "quantity": null, + "unit": null, + "text_quantity": "- 青红椒(根据受辣程度选择, 0-30g)*注:不建议使用肉厚的菜椒", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗 1 把", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 5ml", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 10ml", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅烧热,用手将五花肉紧紧压在锅上炙皮" + }, + { + "step": 2, + "description": "用钢丝球把皮刷干净,至黑色部分碳化部分被完全去除,不刷干净会有苦味" + }, + { + "step": 3, + "description": "将五花肉放入锅中,放入能淹没五花肉的冷水,放入生姜片、料酒和小葱(取 2 棵小葱打结)" + }, + { + "step": 4, + "description": "开大火煮,水开后撇去浮沫,继续煮 15 分钟,煮至瘦肉部分可以用筷子轻松刺穿" + }, + { + "step": 5, + "description": "青红椒切圈" + }, + { + "step": 6, + "description": "蒜苗切段" + }, + { + "step": 7, + "description": "生姜切小薄片" + }, + { + "step": 8, + "description": "将 5ml 豆瓣酱和 5ml 生抽提前混合" + }, + { + "step": 9, + "description": "将煮熟的五花肉捞出放入冷水晾凉" + }, + { + "step": 10, + "description": "擦干五花肉的水,切成上肥下瘦的 2mm 的薄片(切厚了口感不好,而且很油)" + }, + { + "step": 11, + "description": "选用冰冻五花肉常量放置 0.5 小时 或者鲜五花肉放冰箱冷藏 1 个小时,切成 2-5 mm 薄片" + }, + { + "step": 12, + "description": "开中火,辣椒放过锅中干煸 30-45 秒后取出" + }, + { + "step": 13, + "description": "锅烧热,放入一层底油滑锅,放入姜片煸炒 15 秒" + }, + { + "step": 14, + "description": "倒入五花肉,间隔 10 S 翻炒一次,待五花肉出现焦黄色(翻炒时间越久五花肉口感越硬)" + }, + { + "step": 15, + "description": "倒入之前干煸过的辣椒、10ml 豆瓣酱,生抽调味,继续翻炒 60 秒" + }, + { + "step": 16, + "description": "倒入切成段的蒜苗翻炒 10 秒" + }, + { + "step": 17, + "description": "出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-土豆炖排骨-土豆炖排骨", + "name": "土豆炖排骨的做法", + "description": "# 土豆炖排骨的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/土豆炖排骨/土豆炖排骨.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/土豆炖排骨/排骨1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/土豆炖排骨/排骨1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/土豆炖排骨/排骨2.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "肋排", + "quantity": null, + "unit": null, + "text_quantity": "- 肋排", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "黄豆酱", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱", + "notes": "量未指定" + }, + { + "name": "肋排 =", + "quantity": null, + "unit": null, + "text_quantity": "- 肋排 = 750g", + "notes": "量未指定" + }, + { + "name": "土豆 =", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 = 300g", + "notes": "量未指定" + }, + { + "name": "姜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 = 30g", + "notes": "量未指定" + }, + { + "name": "小葱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 = 25g", + "notes": "量未指定" + }, + { + "name": "料酒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 = 25g", + "notes": "量未指定" + }, + { + "name": "白糖 =", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 = 10g", + "notes": "量未指定" + }, + { + "name": "干辣椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 = 5g", + "notes": "量未指定" + }, + { + "name": "八角 =", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 = 5g", + "notes": "量未指定" + }, + { + "name": "花椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 = 5g", + "notes": "量未指定" + }, + { + "name": "桂皮 =", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 = 5g", + "notes": "量未指定" + }, + { + "name": "生抽 =", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 = 10g", + "notes": "量未指定" + }, + { + "name": "老抽 =", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 = 5g", + "notes": "量未指定" + }, + { + "name": "蚝油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 = 5g", + "notes": "量未指定" + }, + { + "name": "黄豆酱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱 = 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "土豆两个滚刀切片,姜片切片" + }, + { + "step": 2, + "description": "排骨 750g 冷水下锅,加入姜片、葱段、料酒焯水 2 分钟,焯干水后捞出清洗干净(一定要用热水,不能用冷水)" + }, + { + "step": 3, + "description": "热锅凉油,将白糖倒入锅中,翻炒至融化为焦糖色" + }, + { + "step": 4, + "description": "加入排骨煎至两面金黄,让排骨裹满焦糖" + }, + { + "step": 5, + "description": "加入干辣椒、八角、花椒、桂皮、姜片(建议买超市的香料包)、10ml 生抽、5ml 老抽、5ml 料酒、5ml 蚝油、5ml 黄豆酱" + }, + { + "step": 6, + "description": "大火翻炒均匀后加入 700ml 开水,大火烧开后转小火焖煮 1 小时" + }, + { + "step": 7, + "description": "最后加入土豆煮 10 分钟就可以出锅啦(喜欢吃青红椒的也可以按自己喜好加入)" + }, + { + "step": 8, + "description": "![成果展示](./排骨1.jpg)" + }, + { + "step": 9, + "description": "![成果展示](./排骨2.jpg)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-奶酪培根通心粉-奶酪培根通心粉", + "name": "奶酪培根通心粉的做法", + "description": "# 奶酪培根通心粉的做法\n\n![烘烤成品](./oven.jpg)\n![非烘烤成品](./onepot.png)\n\n这是一道美味的奶酪培根通心粉(Mac and Cheese),适合四人享用。它结合了浓郁的奶酪和香脆的培根,简单易做,是一道受欢迎的美式家常菜。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/奶酪培根通心粉/奶酪培根通心粉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/奶酪培根通心粉/onepot.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/奶酪培根通心粉/onepot.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/奶酪培根通心粉/oven.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "通心粉", + "quantity": null, + "unit": null, + "text_quantity": "- 通心粉", + "notes": "量未指定" + }, + { + "name": "奶酪", + "quantity": null, + "unit": null, + "text_quantity": "- 奶酪", + "notes": "量未指定" + }, + { + "name": "肉类", + "quantity": null, + "unit": null, + "text_quantity": "- 肉类", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "通心粉", + "quantity": null, + "unit": null, + "text_quantity": "- 通心粉 100-125g", + "notes": "量未指定" + }, + { + "name": "奶酪", + "quantity": null, + "unit": null, + "text_quantity": "- 奶酪 40-55g,若要烘烤额外准备 25g, 条状", + "notes": "量未指定" + }, + { + "name": "培根或其他肉类", + "quantity": null, + "unit": null, + "text_quantity": "- 培根或其他肉类 100-125g", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 25g-40g 切成碎", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 15g", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 10g", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 125ml", + "notes": "量未指定" + }, + { + "name": "大蒜半瓣,切碎", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜半瓣,切碎", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "奶酪要磨成碎末" + }, + { + "step": 2, + "description": "洋葱切成条状" + }, + { + "step": 3, + "description": "通心粉用微咸的水煮 6 分钟" + }, + { + "step": 4, + "description": "**中火**" + }, + { + "step": 5, + "description": "锅中放入黄油,等待融化" + }, + { + "step": 6, + "description": "加入洋葱" + }, + { + "step": 7, + "description": "洋葱软化后加入大蒜" + }, + { + "step": 8, + "description": "大蒜香味出来后,加入肉类,等待 5 秒" + }, + { + "step": 9, + "description": "**小火**" + }, + { + "step": 10, + "description": "分四次加入牛奶,每次搅拌 5 秒后再加下一次" + }, + { + "step": 11, + "description": "加入面粉并充分搅拌" + }, + { + "step": 12, + "description": "加入奶酪并搅拌均匀" + }, + { + "step": 13, + "description": "将通心粉和奶酪搅拌" + }, + { + "step": 14, + "description": "如果不打算烘烤,可以直接吃了" + }, + { + "step": 15, + "description": "**烘烤:**" + }, + { + "step": 16, + "description": "预热烤箱至 180°C" + }, + { + "step": 17, + "description": "将额外的 50g 芝士铺在通心粉之上" + }, + { + "step": 18, + "description": "等待烤箱预热至 180°C 后,将通心粉放入" + }, + { + "step": 19, + "description": "烤至表面金黄,约 24 分钟" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-姜炒鸡-姜炒鸡", + "name": "姜炒鸡的做法", + "description": "# 姜炒鸡的做法\n\n![姜炒鸡](./姜炒鸡.jpg)\n\n姜炒鸡是一道湖南口味菜,下饭五颗星,食材平平无奇又十分容易烹制,一学就会。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/姜炒鸡/姜炒鸡.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/姜炒鸡/姜炒鸡.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/姜炒鸡/姜炒鸡.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "美人辣", + "quantity": null, + "unit": null, + "text_quantity": "- 美人辣", + "notes": "量未指定" + }, + { + "name": "泡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 泡椒", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "鸡 = 半只(土鸡最好,预计", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡 = 半只(土鸡最好,预计 650g)", + "notes": "量未指定" + }, + { + "name": "食用油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 = 50ml(茶油最好,没有就用菜籽油)", + "notes": "量未指定" + }, + { + "name": "生姜 = 半斤 (250g)", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 = 半斤 (250g)", + "notes": "量未指定" + }, + { + "name": "啤酒 = 半瓶", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒 = 半瓶 250ml", + "notes": "量未指定" + }, + { + "name": "生抽 =", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 = 20ml", + "notes": "量未指定" + }, + { + "name": "老抽 =", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 = 10ml", + "notes": "量未指定" + }, + { + "name": "盐 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = 3g", + "notes": "量未指定" + }, + { + "name": "小米椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 = 0-5 个 (0-50g)(根据辣口味调整)", + "notes": "量未指定" + }, + { + "name": "美人辣 =", + "quantity": null, + "unit": null, + "text_quantity": "- 美人辣 = 0-5 个 (0-50g)(没有可以用小米椒代替)", + "notes": "量未指定" + }, + { + "name": "泡椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 泡椒 = 5 个 (50g)", + "notes": "量未指定" + }, + { + "name": "大蒜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 = 3 头 (50g)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡尽量剁成 1cm 的小块,洗净后滤干,再放生抽腌和料酒腌制 30 分钟" + }, + { + "step": 2, + "description": "大先热锅到微微冒烟,放入食用油,等 5 秒" + }, + { + "step": 3, + "description": "下入姜片后转中火炒 30 秒," + }, + { + "step": 4, + "description": "下入鸡块翻炒 3 分钟,炒干水分,炒出鸡油" + }, + { + "step": 5, + "description": "放入各种剁碎的辣椒和大蒜子,加盐和老抽继续翻炒 30 秒" + }, + { + "step": 6, + "description": "倒入啤酒,中小火焖 2 分钟" + }, + { + "step": 7, + "description": "大火收汁盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-姜葱捞鸡-姜葱捞鸡", + "name": "姜葱捞鸡的做法", + "description": "# 姜葱捞鸡的做法\n\n嫩滑爆汁,白饭杀手,简单易做,\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/姜葱捞鸡/姜葱捞鸡.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡腿肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿肉", + "notes": "量未指定" + }, + { + "name": "盐焗鸡粉", + "quantity": null, + "unit": null, + "text_quantity": "- 盐焗鸡粉", + "notes": "量未指定" + }, + { + "name": "葱,姜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱,姜", + "notes": "量未指定" + }, + { + "name": "鸡腿", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿 4 个, 约 400g", + "notes": "量未指定" + }, + { + "name": "盐焗鸡粉", + "quantity": null, + "unit": null, + "text_quantity": "- 盐焗鸡粉 5g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 50g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1 根", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5g", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 5g", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 35ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "四个鸡腿清洗干净,放入碗中" + }, + { + "step": 2, + "description": "碗中加入盐焗鸡粉和 5ml 油,搅拌均匀" + }, + { + "step": 3, + "description": "让鸡腿静置腌制 15 分钟, 同时准备蒸锅并把水煮开" + }, + { + "step": 4, + "description": "鸡腿腌制完成后, 放入水开后的蒸锅中,蒸制 20 分钟" + }, + { + "step": 5, + "description": "将姜根据个人口味切成 1)姜蓉或 2)姜丝或 3)姜粒" + }, + { + "step": 6, + "description": "将葱切成 0.5cm 小段" + }, + { + "step": 7, + "description": "将葱姜放入蘸料碗,并加入盐和糖" + }, + { + "step": 8, + "description": "将剩余的油倒入另一个锅中加热至六至七层热" + }, + { + "step": 9, + "description": "将热油淋入葱姜碗中" + }, + { + "step": 10, + "description": "鸡腿蒸好后将其撕碎成鸡丝,不需要特别细,大概 1cm 粗就可以" + }, + { + "step": 11, + "description": "姜葱姜油淋在鸡丝上,搅拌均匀即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。", + "做法参考:[白饭杀手来啦 #姜葱捞鸡-哔哩哔哩](https://b23.tv/2trBdqJ)" + ] + }, + { + "id": "dishes-meat_dish-宫保鸡丁-宫保鸡丁", + "name": "宫保鸡丁的做法", + "description": "# 宫保鸡丁的做法\n\n老派川菜的简单做法分享\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/宫保鸡丁/宫保鸡丁.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/宫保鸡丁/宫保鸡丁.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/宫保鸡丁/宫保鸡丁.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "手枪腿(或者鸡胸脯肉)", + "quantity": null, + "unit": null, + "text_quantity": "- 手枪腿(或者鸡胸脯肉)", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "干辣椒(或者二荆条)", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒(或者二荆条)", + "notes": "量未指定" + }, + { + "name": "熟花生", + "quantity": null, + "unit": null, + "text_quantity": "- 熟花生", + "notes": "量未指定" + }, + { + "name": "生抽酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油", + "notes": "量未指定" + }, + { + "name": "芝麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油", + "notes": "量未指定" + }, + { + "name": "必须配料", + "quantity": null, + "unit": null, + "text_quantity": "- 必须配料", + "notes": "量未指定" + }, + { + "name": "手枪腿(或者鸡胸脯肉) =", + "quantity": null, + "unit": null, + "text_quantity": "- 手枪腿(或者鸡胸脯肉) = 1 支(约 350g)", + "notes": "量未指定" + }, + { + "name": "大葱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 = 1 根(约 180g)", + "notes": "量未指定" + }, + { + "name": "熟花生 =", + "quantity": null, + "unit": null, + "text_quantity": "- 熟花生 = 150g", + "notes": "量未指定" + }, + { + "name": "姜片 =", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片 = 10g", + "notes": "量未指定" + }, + { + "name": "干辣椒(或者二荆条) =", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒(或者二荆条) = 10g(若选择二荆条,则需要大约 4 支)", + "notes": "量未指定" + }, + { + "name": "生抽酱油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽酱油 = 10g", + "notes": "量未指定" + }, + { + "name": "白糖 =", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 = 2g", + "notes": "量未指定" + }, + { + "name": "盐 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = 2g", + "notes": "量未指定" + }, + { + "name": "植物油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油 = 20g", + "notes": "量未指定" + }, + { + "name": "淀粉 =", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 = 15g", + "notes": "量未指定" + }, + { + "name": "料酒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 = 15g", + "notes": "量未指定" + }, + { + "name": "进阶配料", + "quantity": null, + "unit": null, + "text_quantity": "- 进阶配料", + "notes": "量未指定" + }, + { + "name": "老抽酱油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽酱油 = 5g", + "notes": "量未指定" + }, + { + "name": "花椒 =", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 = 5g", + "notes": "量未指定" + }, + { + "name": "香醋 =", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 = 5g", + "notes": "量未指定" + }, + { + "name": "鸡精 =", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 = 2g", + "notes": "量未指定" + }, + { + "name": "芝麻油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油 = 10g", + "notes": "量未指定" + }, + { + "name": "淀粉(用以勾芡) =", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉(用以勾芡) = 10g", + "notes": "量未指定" + }, + { + "name": "豆瓣酱 =", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 = 10g", + "notes": "量未指定" + }, + { + "name": "可选配料", + "quantity": null, + "unit": null, + "text_quantity": "- 可选配料", + "notes": "量未指定" + }, + { + "name": "莴笋 = 约", + "quantity": null, + "unit": null, + "text_quantity": "- 莴笋 = 约 250g", + "notes": "量未指定" + }, + { + "name": "油泼辣子 =", + "quantity": null, + "unit": null, + "text_quantity": "- 油泼辣子 = 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "手枪腿用剪刀去骨,鸡肉面用刀背拍打一遍,切条后切至 1.5cm 见方肉丁;泡于清水 10 分钟,捞出控干备用(若是鸡胸脯肉,则可以直接进行切丁以及之后的动作)" + }, + { + "step": 2, + "description": "取大葱葱绿与姜片 5g 于碗中,倒入 50g 开水备用;葱白切 1.5cm 圆粒备用;取花生放入微波炉高火 5 分钟焙干备用" + }, + { + "step": 3, + "description": "鸡丁中加入盐 2g,老抽酱油 5g,料酒 15g,淀粉 15g 搅拌均匀,至微微发干;缓慢加入部分葱姜水,搅拌鸡丁至粘手;保鲜膜密封,放入冰箱腌制 1 小时" + }, + { + "step": 4, + "description": "干辣椒切段;起锅,大火烧热转小火;放入干辣椒焙干至微微发糊,捞起备用;花椒焙干至有香味,捞起备用" + }, + { + "step": 5, + "description": "转大火,倒入 20g 植物油,7 成热(竹筷子起泡)下入鸡丁,煎至上面开始发白,用锅铲翻面,煎 30s 后翻炒均匀" + }, + { + "step": 6, + "description": "下入葱粒翻炒,加入余下葱姜水不够 100g 再加一点清水(务必是热水);盖上锅盖,转中小火焖 2 分钟;" + }, + { + "step": 7, + "description": "转大火,下入熟花生,干辣椒和花椒;加入鸡精 2g,香醋 5g,白糖 2g,翻炒均匀;" + }, + { + "step": 8, + "description": "淀粉 10g 加 50g 清水调成水淀粉,加入锅中,翻炒均匀,收汁到自己想要的浓度" + }, + { + "step": 9, + "description": "关火,淋入芝麻油 10g,即可出锅" + }, + { + "step": 10, + "description": "莴笋去皮切至 1cm 见方的小块,备用;" + }, + { + "step": 11, + "description": "二荆条切成 1cm 长段;" + }, + { + "step": 12, + "description": "手枪腿用剪刀去骨,鸡肉面用刀背拍打一遍,切条后切至 1.5cm 见方肉丁;泡于清水 10 分钟,捞出控干备用(若是鸡胸脯肉,则可以直接进行切丁以及之后的动作);" + }, + { + "step": 13, + "description": "取大葱葱绿与姜片 5g 于碗中,倒入 50g 开水备用;葱白切 1.5cm 圆粒备用" + }, + { + "step": 14, + "description": "鸡丁中加入盐 2g,老抽酱油 5g,料酒 15g,淀粉 15g 搅拌均匀,至微微发干;缓慢加入部分葱姜水,搅拌鸡丁至粘手;保鲜膜密封,放入冰箱腌制 1 小时" + }, + { + "step": 15, + "description": "转中火,倒入 20g 植物油,放入生花生翻炒至其表面微微焦糊,捞起花生但是油留在锅内;" + }, + { + "step": 16, + "description": "继续加热,7 成热(竹筷子起泡)下入鸡丁,放入豆瓣酱,翻炒大概 1 分钟;" + }, + { + "step": 17, + "description": "加入备好的莴笋丁,继续翻炒 1 分钟;" + }, + { + "step": 18, + "description": "下入葱粒翻炒,加入余下葱姜水不够 100g 再加一点清水(务必是热水);加入二荆条段;盖上锅盖,转中小火焖 2 分钟;" + }, + { + "step": 19, + "description": "转大火,下入先前捞起来备用的花生,花椒;加入鸡精 2g,香醋 5g,白糖 2g,翻炒均匀;" + }, + { + "step": 20, + "description": "淀粉 10g 加 50g 清水调成水淀粉,加入锅中,翻炒均匀,收汁到自己想要的浓度" + }, + { + "step": 21, + "description": "关火,淋入芝麻油 10g 与油泼辣子 5g 再翻炒 10s,即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-小炒鸡肝-小炒鸡肝", + "name": "小炒鸡肝的做法", + "description": "# 小炒鸡肝的做法\n\n![小炒鸡肝](./成品.jpg)\n\n一道稍微麻烦的菜。\n\n适合喜欢吃肝的人,也可以用其他的动物的肝,但是鸡肝会更好吃。\n\n需要初学者具有一定的焯水技巧。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/小炒鸡肝/小炒鸡肝.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒鸡肝/成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒鸡肝/成品.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "生鸡肝", + "quantity": null, + "unit": null, + "text_quantity": "- 生鸡肝", + "notes": "量未指定" + }, + { + "name": "蒜苗(蒜苗指的是:大蒜幼苗发育到一定时期的青苗。有些地方叫做青蒜,特别说明一下。)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗(蒜苗指的是:大蒜幼苗发育到一定时期的青苗。有些地方叫做青蒜,特别说明一下。)", + "notes": "量未指定" + }, + { + "name": "大葱、姜、料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱、姜、料酒", + "notes": "量未指定" + }, + { + "name": "食用盐、鸡精(味精)、五香粉(十三香)、胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐、鸡精(味精)、五香粉(十三香)、胡椒粉", + "notes": "量未指定" + }, + { + "name": "烧烤料或孜然粉(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 烧烤料或孜然粉(可选)", + "notes": "量未指定" + }, + { + "name": "芝麻(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻(可选)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生鸡肝", + "quantity": null, + "unit": null, + "text_quantity": "- 生鸡肝 5 个(约 800g)", + "notes": "量未指定" + }, + { + "name": "蒜苗(约", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗(约 200g,喜欢吃的可以多放)", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 150g(分成两份,一份切段 100g,一份切片 50g)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 120g(分成两份,一份切片 70g,一份切丁 50g)", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 30ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 5g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 5g(也可以是味精)", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 5g(十三香)", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 5g", + "notes": "量未指定" + }, + { + "name": "烧烤料或孜然粉", + "quantity": null, + "unit": null, + "text_quantity": "- 烧烤料或孜然粉 10g(可选)", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻 5g (可选)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 30ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡肝清洗,备用" + }, + { + "step": 2, + "description": "蒜苗清洗,切段,备用" + }, + { + "step": 3, + "description": "大葱清洗,取 100g 切段,取 50g 切片,备用" + }, + { + "step": 4, + "description": "姜清晰,取 70g 切片, 取 50g 切丁,备用" + }, + { + "step": 5, + "description": "第一步:焯水" + }, + { + "step": 6, + "description": "第二步:炒制" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-小炒黄牛肉-小炒黄牛肉", + "name": "小炒黄牛肉的做法", + "description": "# 小炒黄牛肉的做法\n\n![小炒黄牛肉成品](./小炒黄牛肉.jpg)\n\n小炒黄牛肉是一道简单易做的湘菜。口味十分劲爆爽口。一般初学者只需要 1 小时即可完成\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/小炒黄牛肉/小炒黄牛肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒黄牛肉/小炒黄牛肉.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒黄牛肉/小炒黄牛肉.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛里脊", + "quantity": null, + "unit": null, + "text_quantity": "- 牛里脊", + "notes": "量未指定" + }, + { + "name": "芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "野山椒", + "quantity": null, + "unit": null, + "text_quantity": "- 野山椒", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "牛里脊", + "quantity": null, + "unit": null, + "text_quantity": "- 牛里脊 400g", + "notes": "量未指定" + }, + { + "name": "芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜 200g", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 30g", + "notes": "量未指定" + }, + { + "name": "野山椒", + "quantity": null, + "unit": null, + "text_quantity": "- 野山椒 30g", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 30g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15ml", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 6ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "牛里脊切成不超过 3cm 宽,3mm 厚的薄片,倒入 6ml 酱油,用手抓匀备用" + }, + { + "step": 2, + "description": "芹菜切成不超过 5cm 的小段,备用" + }, + { + "step": 3, + "description": "小米椒切成丝状,备用" + }, + { + "step": 4, + "description": "野山椒切成颗粒,备用" + }, + { + "step": 5, + "description": "香菜切成成不超过 3cm 的小段,备用" + }, + { + "step": 6, + "description": "热锅,锅内放入 15ml 食用油,大火等待 30 秒让油温升高" + }, + { + "step": 7, + "description": "放入小米椒和野山椒爆香" + }, + { + "step": 8, + "description": "放入牛里脊和芹菜,然后**大火翻炒 1 分钟**" + }, + { + "step": 9, + "description": "关火,撒上香菜,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-尖叫牛蛙-尖叫牛蛙", + "name": "尖叫牛蛙的做法", + "description": "# 尖叫牛蛙的做法\n\n![示例菜成品](./尖叫牛蛙.jpg)\n\n尖叫牛蛙是一道容易完成的菜。一般初学者只需要 1-2 小时即可完成。该菜品味道鲜美之外,还具有开胃功效,非常适宜食欲不佳的时候做,老少皆宜。(能吃辣最好)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/尖叫牛蛙/尖叫牛蛙.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/尖叫牛蛙/尖叫牛蛙.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/尖叫牛蛙/尖叫牛蛙.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛蛙肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛蛙肉", + "notes": "量未指定" + }, + { + "name": "泡姜", + "quantity": null, + "unit": null, + "text_quantity": "- 泡姜", + "notes": "量未指定" + }, + { + "name": "泡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 泡椒", + "notes": "量未指定" + }, + { + "name": "野山椒(也可以用干红辣椒替代)", + "quantity": null, + "unit": null, + "text_quantity": "- 野山椒(也可以用干红辣椒替代)", + "notes": "量未指定" + }, + { + "name": "青红辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青红辣椒", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "豆瓣酱(推荐郫县豆瓣)", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱(推荐郫县豆瓣)", + "notes": "量未指定" + }, + { + "name": "盐巴", + "quantity": null, + "unit": null, + "text_quantity": "- 盐巴", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "生粉(干淀粉也可)", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉(干淀粉也可)", + "notes": "量未指定" + }, + { + "name": "啤酒(推荐雪花)", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒(推荐雪花)", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "藤椒油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 藤椒油(可选)", + "notes": "量未指定" + }, + { + "name": "猪油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪油(可选)", + "notes": "量未指定" + }, + { + "name": "葱花", + "quantity": null, + "unit": null, + "text_quantity": "- 葱花", + "notes": "量未指定" + }, + { + "name": "牛蛙肉块", + "quantity": null, + "unit": null, + "text_quantity": "- 牛蛙肉块 800g (买 3 斤活蛙,建议挑小个头的,肉质鲜嫩)", + "notes": "量未指定" + }, + { + "name": "泡姜", + "quantity": null, + "unit": null, + "text_quantity": "- 泡姜 20-30 克 (看个人口味,喜欢重口一点可以多放)", + "notes": "量未指定" + }, + { + "name": "泡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 泡椒 5-10 克 (看个人吃辣的承受能力,不能吃辣的建议减半为 2.5 克,微微辣)", + "notes": "量未指定" + }, + { + "name": "野山椒", + "quantity": null, + "unit": null, + "text_quantity": "- 野山椒 10 克", + "notes": "量未指定" + }, + { + "name": "青红辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青红辣椒 20 克", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 30-50 克 (可以根据口味偏好调整,最好不低于 30 克)", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 20-30 克 (重口选 30,想吃清淡点 20 克。PS 这菜不可能太清淡,哈哈)", + "notes": "量未指定" + }, + { + "name": "盐巴", + "quantity": null, + "unit": null, + "text_quantity": "- 盐巴 15 克", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 10 克", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒 400-500ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10ml", + "notes": "量未指定" + }, + { + "name": "藤椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 藤椒油 5-10ml", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉 30 克 (干淀粉可以平替)", + "notes": "量未指定" + }, + { + "name": "猪油", + "quantity": null, + "unit": null, + "text_quantity": "- 猪油 20ml(没有猪油也可,用食用油替代)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 200ml", + "notes": "量未指定" + }, + { + "name": "葱花", + "quantity": null, + "unit": null, + "text_quantity": "- 葱花 5 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "牛蛙肉洗净后控干水分,加入 10 克以上的盐巴和 50ml 以上的啤酒,用手抓 5 分钟,去除牛蛙肉的腥味" + }, + { + "step": 2, + "description": "然后对着清水冲洗,直至不再流出血水和杂质,控干水分,放到合适的器皿中,准备腌制" + }, + { + "step": 3, + "description": "加入 5 克盐,30 克生粉,10ml 料酒,5 克胡椒粉,用手抓均匀,腌制 5-10 分钟" + }, + { + "step": 4, + "description": "将泡姜 泡椒 野山椒 切丝或者片(根据自己刀工选择),青红辣椒切成圈圈 大蒜拨开即可" + }, + { + "step": 5, + "description": "起锅烧热,加入 200ml 食用油(锅底比较平的可以再加 100ml),烧至 6 成油温(有小气泡出现),将腌制好的牛蛙倒入,快速过油炸制,10 秒钟后捞出(不能超时太多,否则会导致蛙肉老柴)" + }, + { + "step": 6, + "description": "捞出蛙肉后,控油,并将锅中的热油倒出到碗中,保留 30ml,加入 20ml 猪油(如果没有,则在锅中保留总共 50ml 食用油)" + }, + { + "step": 7, + "description": "待油温 6 成热,加入泡姜、泡椒、野山椒、大蒜,超出香味,加入豆瓣酱 20 克,中火翻炒至出红油(时间控制在 30 秒),倒入 400ml 啤酒," + }, + { + "step": 8, + "description": "然后倒入炸过的牛蛙肉,用勺子推着翻,不要用力搅拌,加入 5 克胡椒粉,加入 5ml 藤椒油,中火慢焖 3 分钟" + }, + { + "step": 9, + "description": "加大火力,大火收汁半分钟,加入青红辣椒圈,再煮 10 秒准备起锅" + }, + { + "step": 10, + "description": "盛到盆里,撒上葱花,可以开动了!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-巴基斯坦牛肉咖喱-巴基斯坦牛肉咖喱", + "name": "巴基斯坦牛肉咖喱的做法", + "description": "# 巴基斯坦牛肉咖喱的做法\n\n![巴基斯坦牛肉咖喱成品](./巴基斯坦牛肉咖喱.png)\n\nAchar gosht(巴基斯坦牛肉咖喱)是一道来自巴基斯坦的特色咖喱菜品。这道菜融合了咖喱的香浓和牛肉的软糯口感,风味独特,偏辣口。它富含优质蛋白质和多种维生素,营养价值丰富。制作过程需要 2.5 小时,步骤并不复杂,是一道适合在周末慢慢烹饪的美味佳肴。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/巴基斯坦牛肉咖喱/巴基斯坦牛肉咖喱.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/倒入番茄蓉.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/倒入番茄蓉.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/巴基斯坦牛肉咖喱.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/油.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/牛肉.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/番茄蓉.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/红.png" + ], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "普通的炒锅", + "quantity": null, + "unit": null, + "text_quantity": "- 普通的炒锅", + "notes": "量未指定" + }, + { + "name": "电饭煲/电炖锅", + "quantity": null, + "unit": null, + "text_quantity": "- 电饭煲/电炖锅", + "notes": "量未指定" + }, + { + "name": "Masala 粉(品牌可选 Shan)", + "quantity": null, + "unit": null, + "text_quantity": "- Masala 粉(品牌可选 Shan)", + "notes": "量未指定" + }, + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉", + "notes": "量未指定" + }, + { + "name": "番茄", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄", + "notes": "量未指定" + }, + { + "name": "螺丝椒", + "quantity": null, + "unit": null, + "text_quantity": "- 螺丝椒", + "notes": "量未指定" + }, + { + "name": "原味酸奶", + "quantity": null, + "unit": null, + "text_quantity": "- 原味酸奶", + "notes": "量未指定" + }, + { + "name": "蒜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜粉", + "notes": "量未指定" + }, + { + "name": "姜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 姜粉", + "notes": "量未指定" + }, + { + "name": "番茄🍅", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄🍅 4 个", + "notes": "量未指定" + }, + { + "name": "螺丝椒", + "quantity": null, + "unit": null, + "text_quantity": "- 螺丝椒 2 个(大个的)", + "notes": "量未指定" + }, + { + "name": "原味酸奶", + "quantity": null, + "unit": null, + "text_quantity": "- 原味酸奶 1 盒", + "notes": "量未指定" + }, + { + "name": "Masala 粉一包", + "quantity": null, + "unit": null, + "text_quantity": "- Masala 粉一包 50g", + "notes": "量未指定" + }, + { + "name": "蒜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜粉 5g", + "notes": "量未指定" + }, + { + "name": "姜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 姜粉 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "全部螺丝椒切成段状,备用" + }, + { + "step": 2, + "description": "全部番茄打成番茄蓉,备用" + }, + { + "step": 3, + "description": "牛肉切成 2cm 的小块,洗净备用" + }, + { + "step": 4, + "description": "炒锅中倒入一层油(用来防止番茄蓉沸腾蒸发)" + }, + { + "step": 5, + "description": "倒入番茄蓉,持续搅拌 2-3 分钟,等待它越变越红" + }, + { + "step": 6, + "description": "加入 5g 蒜粉,5g 姜粉和 1 包 50g 的 Masala 粉,搅拌均匀" + }, + { + "step": 7, + "description": "加入牛肉和螺丝椒段,搅拌均匀" + }, + { + "step": 8, + "description": "加入 1 盒酸奶(为了让整个酱汁变得粘稠),搅拌均匀" + }, + { + "step": 9, + "description": "将整锅材料转移到电饭煲/电炖锅,并加入 250 ml 的水,开启炖肉/慢炖档,设定时间 2-3 个小时" + }, + { + "step": 10, + "description": "等待完成,开锅检查牛肉软糯,就可以吃了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-干煸仔鸡-干煸仔鸡", + "name": "干煸仔鸡的做法", + "description": "# 干煸仔鸡的做法\n\n![干煸仔鸡成品](./干煸仔鸡成品.jpg)\n\n干煸仔鸡是一道甜辣口味的川菜,是北京大学食堂赵春月厨师长研发的美食,广受师生喜爱。赵厨师长已将菜谱公开,方便大家自己动手制作,疫情居家下饭必备!\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/干煸仔鸡/干煸仔鸡.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/干煸仔鸡/干煸仔鸡成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/干煸仔鸡/干煸仔鸡成品.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡腿", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "郫县红油豆瓣酱(注意区分,不是那种棕黄色的豆瓣酱)", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县红油豆瓣酱(注意区分,不是那种棕黄色的豆瓣酱)", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "花椒碎", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒碎", + "notes": "量未指定" + }, + { + "name": "鸡腿肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡腿肉 400g", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 200g", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 60g", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 10g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 3g (可选)", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 2g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 2g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 5g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 20g", + "notes": "量未指定" + }, + { + "name": "郫县红油豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县红油豆瓣酱 40g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 30g", + "notes": "量未指定" + }, + { + "name": "花椒碎", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒碎 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡腿去骨(如使用鸡腿排可忽略此步骤),鸡腿肉用刀背砸一砸,切成 2cm 的块。" + }, + { + "step": 2, + "description": "鸡腿肉中加入盐、鸡精(可选)、胡椒粉、生抽、老抽、料酒,抓拌至粘手时加入淀粉拌匀,加入食用油防止粘连,腌制 30 分钟。" + }, + { + "step": 3, + "description": "土豆去皮,切成 2cm 的块,沸水煮 5 分钟后捞出,控干水分,防止油炸时爆锅。" + }, + { + "step": 4, + "description": "青椒去籽,切成 2cm 小片,放在笊篱中备用。" + }, + { + "step": 5, + "description": "锅中加入宽油(根据锅的形状,能没过食材即可),油温烧至 180℃ 时,下入土豆块炸 3 分钟后捞出。" + }, + { + "step": 6, + "description": "待油温再次升高到 180℃时,下入鸡块炸 2 分钟后捞出。" + }, + { + "step": 7, + "description": "待油温再次升高到 180℃时,下入鸡块复炸 1 分钟后捞出。" + }, + { + "step": 8, + "description": "待油温再次升高到 180℃时,下入土豆块复炸 1 分钟后,将锅中的油和土豆块经过笊篱过滤倒出,让笊篱上的青椒片断生。" + }, + { + "step": 9, + "description": "锅中加入 5ml 食用油,小火煸炒蒜瓣至发黄,下入红油豆瓣酱煸炒出香,下入白糖炒融化,下入花椒碎,加 40ml 清水,不停搅拌至酱汁粘稠。" + }, + { + "step": 10, + "description": "下入炸好的鸡块、土豆块、青椒片,搅拌均匀后出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-广式萝卜牛腩-广式萝卜牛腩", + "name": "广式萝卜牛腩的做法", + "description": "# 广式萝卜牛腩的做法\n\n![广式萝卜牛腩](./广式萝卜牛腩.webp)\n\n广式萝卜牛腩营养丰富,味道鲜美,汤汁浓郁、孩子食欲好了,成绩也好了。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/广式萝卜牛腩/广式萝卜牛腩.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/广式萝卜牛腩/广式萝卜牛腩.webp", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/广式萝卜牛腩/广式萝卜牛腩.webp" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛腩", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腩", + "notes": "量未指定" + }, + { + "name": "白萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 白萝卜", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "南乳", + "quantity": null, + "unit": null, + "text_quantity": "- 南乳", + "notes": "量未指定" + }, + { + "name": "柱侯酱", + "quantity": null, + "unit": null, + "text_quantity": "- 柱侯酱", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "牛腩", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腩 500g", + "notes": "量未指定" + }, + { + "name": "白萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 白萝卜 1 根", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片 8 片", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 5 片", + "notes": "量未指定" + }, + { + "name": "葱结 一把", + "quantity": null, + "unit": null, + "text_quantity": "- 葱结 一把", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 2 片", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 1 小块", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 2 个", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 2 片", + "notes": "量未指定" + }, + { + "name": "南乳", + "quantity": null, + "unit": null, + "text_quantity": "- 南乳 1 块", + "notes": "量未指定" + }, + { + "name": "柱侯酱", + "quantity": null, + "unit": null, + "text_quantity": "- 柱侯酱 30g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 15g", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 15g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 10g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 15g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "萝卜滚到切块备用" + }, + { + "step": 2, + "description": "牛腩整块焯水,加入 2 片姜和一把葱结,等水开之后煮 5-10 分钟,然后捞出切件" + }, + { + "step": 3, + "description": "将牛腩切块,切成自己喜欢的大小(牛腩焯过水,待会儿焖的时候基本不会缩水了,大块的会焖相对就一点的时间)" + }, + { + "step": 4, + "description": "准备焖牛腩的酱料,将南乳、柱侯酱、酱油、蚝油、糖、盐按上面的量调和(冰糖刚刚没了换成了白糖)" + }, + { + "step": 5, + "description": "热锅下油,将姜蒜爆香,放入牛腩,炒至干身,加入调好的酱料,炒香,如果喜欢色泽浓一点的可以加一点老抽润色一下" + }, + { + "step": 6, + "description": "调料充分混合之后倒入热水" + }, + { + "step": 7, + "description": "将牛腩换到汤锅中,放入桂皮、八角、香叶和干辣椒,焖大概 2 个小时" + }, + { + "step": 8, + "description": "牛腩焖到半软之后加入白萝卜继续焖 30 分钟" + }, + { + "step": 9, + "description": "等到萝卜焖软之后就完成,一锅浓香的萝卜牛腩就完成了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-徽派红烧肉-徽派红烧肉", + "name": "徽派红烧肉的做法", + "description": "# 徽派红烧肉的做法\n\n徽式红烧肉是一道由五花肉等食材制成的菜肴。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/徽派红烧肉/徽派红烧肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/徽派红烧肉/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/徽派红烧肉/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/徽派红烧肉/2.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 300 g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 100 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 200 g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10 ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5 ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5 ml", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片 2 片", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 3 颗", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 100 ml", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1 根", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 10 g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 10 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "五花肉切块,每块 2-3 cm 大小" + }, + { + "step": 2, + "description": "锅中加入 150 ml 食用油,倒入五花肉,煎炸 2 分钟 后,加入盐,翻炒五花肉,2 分钟 后出锅" + }, + { + "step": 3, + "description": "锅中加入 50 ml 食用油,倒入白砂糖,翻炒到咖啡色" + }, + { + "step": 4, + "description": "倒入五花肉,翻炒 30 S ,加入姜片、蒜头后翻炒 30 S" + }, + { + "step": 5, + "description": "加入料酒,五香粉、葱,加入水没过五花肉,盖上锅盖煮 10 分钟" + }, + { + "step": 6, + "description": "加入生抽、老抽、蚝油,中火煮 20 分钟" + }, + { + "step": 7, + "description": "开锅,大火烧汁,端盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-新疆大盘鸡-新疆大盘鸡", + "name": "新疆大盘鸡的做法", + "description": "# 新疆大盘鸡的做法\n\n![大盘鸡](./大盘鸡.jpeg)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/新疆大盘鸡/新疆大盘鸡.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/新疆大盘鸡/大盘鸡.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/新疆大盘鸡/大盘鸡.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/新疆大盘鸡/大盘鸡皮带面.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "花椒,香叶,香果,干线椒,大蒜,大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒,香叶,香果,干线椒,大蒜,大葱", + "notes": "量未指定" + }, + { + "name": "油,盐,生抽,蚝油,料酒(可拿啤酒),白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 油,盐,生抽,蚝油,料酒(可拿啤酒),白糖", + "notes": "量未指定" + }, + { + "name": "鸡肉(鸡腿肉最好),土豆,菜椒和甜椒(可以不用,加上配色好看)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡肉(鸡腿肉最好),土豆,菜椒和甜椒(可以不用,加上配色好看)", + "notes": "量未指定" + }, + { + "name": "两个火枪腿的鸡肉(这大约是", + "quantity": null, + "unit": null, + "text_quantity": "- 两个火枪腿的鸡肉(这大约是 1kg )", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 2 个适中大小:750g", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 100g", + "notes": "量未指定" + }, + { + "name": "菜椒甜椒各一个,各", + "quantity": null, + "unit": null, + "text_quantity": "- 菜椒甜椒各一个,各 50g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 20g", + "notes": "量未指定" + }, + { + "name": "干线椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干线椒 5 个", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 4 瓣", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 50g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "肉先剁好,块状,用清水+盐浸泡 5 分钟,去除血水,去腥,然后空干水" + }, + { + "step": 2, + "description": "葱蒜辣椒土豆等洗干净,土豆削皮" + }, + { + "step": 3, + "description": "葱白切长段,长度 4cm 一段,菜椒和线椒切块状" + }, + { + "step": 4, + "description": "土豆切成滚刀土豆,即切一刀动滚动一下,一块土豆大概有 4cm*4cm 大小即可" + }, + { + "step": 5, + "description": "炒糖色:先将油加入锅中,然后将白砂糖放入,用锅铲来回搅拌,将糖炒化,然后炒出焦黄色,此时将空干水的鸡肉倒入锅中翻炒,进行上色" + }, + { + "step": 6, + "description": "放入花椒,香叶,香果,干线椒等进行翻炒" + }, + { + "step": 7, + "description": "放入 5g 盐,生抽 7ml,蚝油 10g ,料酒 100g,倒入 1 升清水,料酒可以用啤酒代替" + }, + { + "step": 8, + "description": "调至中火,将水烧开,调制中小火慢炖入味" + }, + { + "step": 9, + "description": "当水收至鸡肉即将露出时,将土豆放在锅表面:注意不要翻动土豆,就盖在表面,不然翻到下面容易粘锅,继续盖锅盖炖,炖一会后将大葱,菜椒和甜椒放入,继续炖。" + }, + { + "step": 10, + "description": "炖到汁收的差不多时可以进行翻面,将土豆与汤汁相吸,最后关火盛出。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-无骨鸡爪-无骨鸡爪", + "name": "无骨鸡爪的做法", + "description": "# 无骨鸡爪的做法\n\n![无骨鸡爪成品](./无骨鸡爪.jpg)\n**图片里的颜色比较浅,家里人爱吃酱油少的**\n\n这是一道做法简单但消耗体力和耐力的无骨鸡爪,酸辣开胃,Q 弹爽口,第一次做的话总耗时 8 个小时 15 分钟。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/无骨鸡爪/无骨鸡爪.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/无骨鸡爪/无骨鸡爪.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/无骨鸡爪/无骨鸡爪.jpg" + ], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡爪", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡爪", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "黑醋(推荐陈醋)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑醋(推荐陈醋)", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "花椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒油", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬", + "notes": "量未指定" + }, + { + "name": "鸡爪", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡爪 1kg", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 4 片", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 65g", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 3 段(5cm 一段)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 10 瓣", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 4 个少辣,6 个中辣,12 个大辣(推荐大辣)", + "notes": "量未指定" + }, + { + "name": "洋葱 (半个)", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 (半个)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 75g = 15g * 5", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 30g = 15g * 2", + "notes": "量未指定" + }, + { + "name": "黑醋(推荐陈醋)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑醋(推荐陈醋) 50g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 10g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "花椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒油 10ml", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 3 颗", + "notes": "量未指定" + }, + { + "name": "柠檬", + "quantity": null, + "unit": null, + "text_quantity": "- 柠檬 2 颗(以 1 颗为单位来调整酸度)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "用剪刀 / 刀 把鸡爪上的指甲的部分全部剪掉 **包括指甲下面的肉和骨头,让它一点指甲都不剩**" + }, + { + "step": 2, + "description": "用水把他们洗干净,放一边" + }, + { + "step": 3, + "description": "把`鸡爪`放入大锅中,准备去腥味" + }, + { + "step": 4, + "description": "`大葱`,`料酒`,`姜` 全放进去" + }, + { + "step": 5, + "description": "加水没过`鸡爪`" + }, + { + "step": 6, + "description": "大火煮开 **中途可以把浮末捞起来**" + }, + { + "step": 7, + "description": "水开**100度,沸腾**后等 10 分钟" + }, + { + "step": 8, + "description": "关火,捞出来,把水沥干,洗干净,放入盆里" + }, + { + "step": 9, + "description": "放入冰箱,**冷冻层** 20 分钟" + }, + { + "step": 10, + "description": "把全部放入不是冷冻层的冰箱,然后分批**10个一批**拿出来去骨" + }, + { + "step": 11, + "description": "从手指(鸡爪的)最前端开始,每只手指都要用刀划开**划到它的手背部分**" + }, + { + "step": 12, + "description": "再从手背部用刀分划开至整个手臂" + }, + { + "step": 13, + "description": "把每只手指关节处都掰一掰**按手指出声音时那种**" + }, + { + "step": 14, + "description": "按着它的手指最前端,往里推,每只手指都一样,先推到中间手掌手背部分" + }, + { + "step": 15, + "description": "每只手指皮脱离后,从手掌开始往手臂部分推直到整个脱下来了" + }, + { + "step": 16, + "description": "放入碗中,备用" + }, + { + "step": 17, + "description": "`小米辣` 切均匀小颗" + }, + { + "step": 18, + "description": "`大蒜`,`洋葱`,`香菜`切碎" + }, + { + "step": 19, + "description": "`柠檬`对半切开,把柠檬汁挤入鸡爪的容器里" + }, + { + "step": 20, + "description": "把`全部`调料倒入装鸡爪的容器,`小米辣`,`大蒜`,`洋葱`和`香菜`也放进去" + }, + { + "step": 21, + "description": "抓拌均匀" + }, + { + "step": 22, + "description": "放入冰箱一个晚上(6 个小时)" + }, + { + "step": 23, + "description": "调配好后全部放入准备好的鸡爪" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-枝竹羊腩煲-枝竹羊腩煲", + "name": "枝竹羊腩煲的做法", + "description": "# 枝竹羊腩煲的做法\n\n枝竹羊腩煲是一份老少皆宜,适合冬季暖胃的美食。 此道菜肥而不腻,搭配米饭堪称一绝。一般初学者需 2 个半小时即可完成。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/枝竹羊腩煲/枝竹羊腩煲.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "羊腩", + "quantity": null, + "unit": null, + "text_quantity": "- 羊腩", + "notes": "量未指定" + }, + { + "name": "腐竹", + "quantity": null, + "unit": null, + "text_quantity": "- 腐竹", + "notes": "量未指定" + }, + { + "name": "柱侯酱", + "quantity": null, + "unit": null, + "text_quantity": "- 柱侯酱", + "notes": "量未指定" + }, + { + "name": "腐乳", + "quantity": null, + "unit": null, + "text_quantity": "- 腐乳", + "notes": "量未指定" + }, + { + "name": "南乳", + "quantity": null, + "unit": null, + "text_quantity": "- 南乳", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "葱段", + "quantity": null, + "unit": null, + "text_quantity": "- 葱段", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片", + "notes": "量未指定" + }, + { + "name": "香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 香菇", + "notes": "量未指定" + }, + { + "name": "洋葱或红葱头", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱或红葱头", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "其余配菜例如马蹄、土豆或者萝卜可依据个人喜好自行添加", + "quantity": null, + "unit": null, + "text_quantity": "- 其余配菜例如马蹄、土豆或者萝卜可依据个人喜好自行添加", + "notes": "量未指定" + }, + { + "name": "羊腩", + "quantity": null, + "unit": null, + "text_quantity": "- 羊腩 500g", + "notes": "量未指定" + }, + { + "name": "炸腐竹", + "quantity": null, + "unit": null, + "text_quantity": "- 炸腐竹 30g-50g", + "notes": "量未指定" + }, + { + "name": "柱侯酱", + "quantity": null, + "unit": null, + "text_quantity": "- 柱侯酱 30g", + "notes": "量未指定" + }, + { + "name": "腐乳", + "quantity": null, + "unit": null, + "text_quantity": "- 腐乳 40g", + "notes": "量未指定" + }, + { + "name": "南乳", + "quantity": null, + "unit": null, + "text_quantity": "- 南乳 35g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5ml", + "notes": "量未指定" + }, + { + "name": "辣椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油 5ml", + "notes": "量未指定" + }, + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水 500ml", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 20g", + "notes": "量未指定" + }, + { + "name": "砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 砂糖 10g", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 5 根", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片 6-8 片", + "notes": "量未指定" + }, + { + "name": "香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 香菇 7-8 个", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 1 个或红葱头 4 - 5 个", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 7-8 瓣", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 1 片", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 4-5 个", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "准备工作: 香菇提前浸泡 2 - 3 小时至变软。腐竹提前浸泡 30 分钟至变软" + }, + { + "step": 2, + "description": "准备酱汁 1: 南乳、柱侯酱、20g 腐乳、老抽放入同个小碗中搅拌均匀" + }, + { + "step": 3, + "description": "准备酱汁 2: 20g 腐乳、砂糖、辣椒油放入同个小碗搅拌均匀" + }, + { + "step": 4, + "description": "泡好的香菇去除根部" + }, + { + "step": 5, + "description": "泡好的腐竹切成 5cm 的小段,挤干水分" + }, + { + "step": 6, + "description": "洋葱去皮切丝。也可以用去皮红葱头进行替代,口味更佳。" + }, + { + "step": 7, + "description": "小葱切成大约 5cm 的葱段" + }, + { + "step": 8, + "description": "羊腩冷水下锅,放入 2 - 3 片姜片,倒入凉水,大火煮至水滚后关火" + }, + { + "step": 9, + "description": "捞出羊腩,放入准备好的冷水盆中放凉,使其更有嚼劲" + }, + { + "step": 10, + "description": "锅烧热后放入冷油,放入 4 - 5 片姜片、洋葱/红葱头、葱白段、7 - 8 瓣蒜瓣进行爆香" + }, + { + "step": 11, + "description": "放入冷却好的羊腩,用筷子搅拌大约 2 - 5 分钟直至出现金黄色" + }, + { + "step": 12, + "description": "放入调好的酱汁 1 ,翻炒大约 2 分钟至颜色均匀" + }, + { + "step": 13, + "description": "倒入清水至刚好没过食材" + }, + { + "step": 14, + "description": "放入香菇、冰糖、香叶、八角、桂皮" + }, + { + "step": 15, + "description": "加盖转小火炖 90 分钟" + }, + { + "step": 16, + "description": "开盖加入腐竹,加盖转中火煮 20 分钟" + }, + { + "step": 17, + "description": "开盖加入酱汁 2 搅拌均匀" + }, + { + "step": 18, + "description": "关火,出锅前加入葱绿段或香菜" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-柱候牛腩-柱候牛腩", + "name": "柱候牛腩的做法", + "description": "# 柱候牛腩的做法\n\n![柱候牛腩成品](./柱候牛腩.jpeg)\n![柱候牛腩搭配米饭](./柱候牛腩配米饭.jpeg)\n\n肉香味美,色泽诱人,滋补强壮,口感甚佳,令人垂涎的广式菜肴。有高压锅只需 1 个小时,否则需要炖煮 3 个小时。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/柱候牛腩/柱候牛腩.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/土豆切片.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/土豆切片.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/柱候牛腩.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/柱候牛腩配米饭.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩入锅.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩切块.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩此时可开始炖煮.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩焯水.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩部位.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗3.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗4.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗5.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗6.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗7.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/过滤汤汁.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "炖煮锅,高压锅(可选,但极度推荐!)", + "quantity": null, + "unit": null, + "text_quantity": "- 炖煮锅,高压锅(可选,但极度推荐!)", + "notes": "量未指定" + }, + { + "name": "牛腩(首选坑腩,带筋的部位)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腩(首选坑腩,带筋的部位)", + "notes": "量未指定" + }, + { + "name": "柱候酱(核心酱),郫县豆瓣酱,南腐乳,叉烧酱(可选),蚝油, 老抽,生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 柱候酱(核心酱),郫县豆瓣酱,南腐乳,叉烧酱(可选),蚝油, 老抽,生抽", + "notes": "量未指定" + }, + { + "name": "花雕酒,白酒", + "quantity": null, + "unit": null, + "text_quantity": "- 花雕酒,白酒", + "notes": "量未指定" + }, + { + "name": "香叶, 花椒,八角,干辣椒,丁香,甘草,干辣椒,小米辣(可选),姜,蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶, 花椒,八角,干辣椒,丁香,甘草,干辣椒,小米辣(可选),姜,蒜", + "notes": "量未指定" + }, + { + "name": "牛腩", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腩 500-600g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 30g", + "notes": "量未指定" + }, + { + "name": "蒜半头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜半头", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 1 条(可根据口味调整用量)", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 2 片", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 0.5g", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 2 个", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 3 个(可根据口味调整用量)", + "notes": "量未指定" + }, + { + "name": "丁香", + "quantity": null, + "unit": null, + "text_quantity": "- 丁香 3 个", + "notes": "量未指定" + }, + { + "name": "甘草", + "quantity": null, + "unit": null, + "text_quantity": "- 甘草 2 片", + "notes": "量未指定" + }, + { + "name": "南腐乳", + "quantity": null, + "unit": null, + "text_quantity": "- 南腐乳 2 块", + "notes": "量未指定" + }, + { + "name": "郫县豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 郫县豆瓣酱 15g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 10g", + "notes": "量未指定" + }, + { + "name": "花雕酒", + "quantity": null, + "unit": null, + "text_quantity": "- 花雕酒 10g", + "notes": "量未指定" + }, + { + "name": "白酒", + "quantity": null, + "unit": null, + "text_quantity": "- 白酒 20g", + "notes": "量未指定" + }, + { + "name": "柱候酱", + "quantity": null, + "unit": null, + "text_quantity": "- 柱候酱 50g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 20g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 60g", + "notes": "量未指定" + }, + { + "name": "叉烧酱", + "quantity": null, + "unit": null, + "text_quantity": "- 叉烧酱 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "先把辅料备好:" + }, + { + "step": 2, + "description": "牛肉不用切,直接冷水下锅,开大火焯水,水沸腾时将牛肉捞出" + }, + { + "step": 3, + "description": "冲洗牛肉表面的杂质后,切成 4cm\\*4cm\\*4cm 的大块,控干水后放入碗中备用" + }, + { + "step": 4, + "description": "大火,热锅下油,把碗 1(姜、蒜、小米辣)倒入锅中,炒香" + }, + { + "step": 5, + "description": "中小火,倒入碗 2(香料),翻炒均匀,大概 30 秒" + }, + { + "step": 6, + "description": "中小火,放入碗 3(南乳),用锅铲把南乳压碎" + }, + { + "step": 7, + "description": "中小火,放入碗 4(豆瓣酱),翻炒均匀,大概 30 秒" + }, + { + "step": 8, + "description": "中小火,放入碗 5(冰糖),炒至融化" + }, + { + "step": 9, + "description": "中小火,下入牛腩,炒至牛肉上色" + }, + { + "step": 10, + "description": "大火,沿锅边淋入碗 6(酒),快速翻炒,炒至牛肉表面略微焦褐" + }, + { + "step": 11, + "description": "倒入碗 7(酱料),快速翻炒,留意底层汁水,炒至不停冒小气泡,汤汁略微浓稠" + }, + { + "step": 12, + "description": "将锅内全部食材转移至另一个炖煮锅或高压锅,加水淹过食材" + }, + { + "step": 13, + "description": "根据使用的锅来选择炖肉的时间:" + }, + { + "step": 14, + "description": "时间到后开盖调味,如果不够咸加盐或生抽(少量加,不断尝味道,直到合适),不够甜则同理加糖" + }, + { + "step": 15, + "description": "调好味道后便可以把牛腩先捞出" + }, + { + "step": 16, + "description": "如果要吃萝卜土豆,则削皮切成 2cm 厚片倒入锅中煮 10 - 15 分钟(或煮至想要吃的口感),如果是高压锅则在加压煮 5 分钟" + }, + { + "step": 17, + "description": "煮好后捞出萝卜土豆和牛腩放一起" + }, + { + "step": 18, + "description": "把汤汁过滤淋入碗中" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。", + "做法参考:[柱候牛腩+茅根马蹄竹蔗水教程](https://www.bilibili.com/video/BV12C4y1W7ox)" + ] + }, + { + "id": "dishes-meat_dish-梅菜扣肉-梅菜扣肉", + "name": "梅菜扣肉的做法", + "description": "# 梅菜扣肉的做法\n\n梅菜扣肉造型别致、大方得体,颜色酱红油亮,汤汁黏稠鲜美,扣肉肥而不腻,食之软烂醇香。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/梅菜扣肉/梅菜扣肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/3.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/4.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "梅菜", + "quantity": null, + "unit": null, + "text_quantity": "- 梅菜", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 200 g", + "notes": "量未指定" + }, + { + "name": "梅菜", + "quantity": null, + "unit": null, + "text_quantity": "- 梅菜 30 g", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 2 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 300 ml", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 5 g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 30 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 20 ml", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 1 个", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末 10 g", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 2 g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 2 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "梅菜放到清水中,浸泡 1 小时" + }, + { + "step": 2, + "description": "锅中倒入 50 ml 食用油,将整个五花肉猪皮朝下,放到锅中 1 分钟 ,取出挂掉猪皮 【可选】" + }, + { + "step": 3, + "description": "锅中加入开水,放入五花肉,大火煮 20 分钟 (筷子可以插进五花肉),取出五花肉" + }, + { + "step": 4, + "description": "在五花肉表面涂抹均匀老抽、五香粉、白砂糖,放置 15 分钟" + }, + { + "step": 5, + "description": "起锅烧油,加入五花肉,中火油炸直至两面金黄色(3-5 分钟)" + }, + { + "step": 6, + "description": "起锅烧油,倒入梅菜,加上小米椒、蒜蓉、鸡精、食用盐后翻炒,直至炒干梅干菜水分" + }, + { + "step": 7, + "description": "五花肉切片(后端 0.5-1 cm),放在大碗中,散上梅干菜" + }, + { + "step": 8, + "description": "中火蒸 45 分钟" + }, + { + "step": 9, + "description": "拿个盘子倒盖在五花肉大碗中,将五花肉倒在盘子中" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-水煮牛肉-水煮牛肉", + "name": "水煮牛肉的做法", + "description": "# 水煮牛肉的做法\n\n![shuizhuniurou](./sznr1.jpg)\n\n麻辣鲜香\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/水煮牛肉/水煮牛肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr10.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr11.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr12.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr2.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr3.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr4.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr5.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr6.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr7.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr8.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr9.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉", + "notes": "量未指定" + }, + { + "name": "豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 豆芽", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "干辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒粉", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "红辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红辣椒", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉 300g", + "notes": "量未指定" + }, + { + "name": "豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 豆芽 100g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 5 根", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 10g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10ml", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 15g", + "notes": "量未指定" + }, + { + "name": "干辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒粉 5g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 20g", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 3 瓣", + "notes": "量未指定" + }, + { + "name": "红辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红辣椒 1 根", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 8g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "牛肉洗干净切片。" + }, + { + "step": 2, + "description": "加入 15g 姜丝,1 个鸡蛋,15g 淀粉,8g 蚝油,10ml 料酒搅拌均匀,腌制 15 分钟。" + }, + { + "step": 3, + "description": "香菜洗干净切好。" + }, + { + "step": 4, + "description": "锅里倒油,加入豆瓣酱,5g 姜丝,蒜片。" + }, + { + "step": 5, + "description": "倒入开水,煮成红汤。" + }, + { + "step": 6, + "description": "豆芽洗干净去掉尾须,放进开水里焯熟。" + }, + { + "step": 7, + "description": "将豆芽铺入碗底。" + }, + { + "step": 8, + "description": "将牛肉片一片一片的放进红汤中,煮熟以后捞出。" + }, + { + "step": 9, + "description": "将牛肉铺在豆芽上,撒上香菜梗。" + }, + { + "step": 10, + "description": "撒上香菜叶,辣椒粉,辣椒圈。" + }, + { + "step": 11, + "description": "另起锅烧热油,将热油淋在菜上面,就完成了。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。", + "做法参考:[水煮牛肉的详细步骤](https://www.zhms.cn/recipe/blrqm.html?source=2)" + ] + }, + { + "id": "dishes-meat_dish-清蒸鳜鱼-清蒸鳜鱼", + "name": "清蒸鳜鱼的做法", + "description": "# 清蒸鳜鱼的做法\n\n![清蒸鳜鱼成品图](./清蒸鳜鱼成品图.jpg)\n\n鳜鱼可以称的上淡水鱼之王,味道鲜美,所谓高端的食材只需要最简单的烹饪方式,清蒸最能体现鳜鱼的鲜美。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/清蒸鳜鱼/清蒸鳜鱼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/清蒸鳜鱼/清蒸鳜鱼成品图.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/清蒸鳜鱼/清蒸鳜鱼成品图.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鳜鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 鳜鱼", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "红辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红辣椒", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "鳜鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 鳜鱼 500g", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 1 节", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 2 根", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 30g", + "notes": "量未指定" + }, + { + "name": "红辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红辣椒 1 颗", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片 3 片", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 20ml", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-湖南家常红烧肉-湖南家常红烧肉", + "name": "湖南家常红烧肉的做法", + "description": "# 湖南家常红烧肉的做法\n\n![湖南家常红烧肉](./湖南家常红烧肉.jpeg)\n\n湖南家常红烧肉,入口软糯,肥而不腻\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/湖南家常红烧肉/湖南家常红烧肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湖南家常红烧肉/湖南家常红烧肉.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湖南家常红烧肉/湖南家常红烧肉.jpeg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "带皮五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 带皮五花肉", + "notes": "量未指定" + }, + { + "name": "干小米椒🌶(根据个人情况而定)", + "quantity": null, + "unit": null, + "text_quantity": "- 干小米椒🌶(根据个人情况而定)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "冰糖(锁油上色)", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖(锁油上色)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水", + "notes": "量未指定" + }, + { + "name": "五花肉:500g", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉:500g", + "notes": "量未指定" + }, + { + "name": "食用油:10g", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油:10g", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 5 片", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 3 片", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 1 小块", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 6 颗", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 20 克", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 克", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 2 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 克", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 3 颗", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "带皮五花肉洗净冷水下锅,加入姜片 2~3 片去腥味,煮到沸腾捞出冷水冲净白沫" + }, + { + "step": 2, + "description": "五花肉切块,尺寸 1.5cm*1.5cm 块状大小" + }, + { + "step": 3, + "description": "热锅加入油,加入冰糖小火搅拌至焦糖色即可,加入切好的五花肉,中火翻炒上色" + }, + { + "step": 4, + "description": "加入备好的姜片、八角、桂皮、生抽、老抽、料酒、干小米椒、盐,小火翻炒 1 分钟,加开水没过肉" + }, + { + "step": 5, + "description": "加盖中火煮沸,转小火慢顿 30 分钟,慢炖期间,间隔 10 分钟搅拌一次防止粘锅" + }, + { + "step": 6, + "description": "小火慢炖汤汁剩三分之一的时,调成中火收汁出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-湘祁米夫鸭-湘祁米夫鸭", + "name": "湘祁米夫鸭的做法", + "description": "# 湘祁米夫鸭的做法\n\n![湘祁米夫鸭](./湘祁米夫鸭.jpg)\n\n湖南两祁地区特色菜品,逢年过节家家桌上有。鸭肉被米粉子包裹,入口咸香回味悠长可解乡愁。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/湘祁米夫鸭/湘祁米夫鸭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step①:准备米粉.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step①:准备米粉.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step②:煸炒鸭子.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step③:米粉裹鸭.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step④:高压锅蒸煮.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/湘祁米夫鸭.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸭子(必须新鲜现杀的)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸭子(必须新鲜现杀的)", + "notes": "量未指定" + }, + { + "name": "糯米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米粉", + "notes": "量未指定" + }, + { + "name": "粘米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 粘米粉", + "notes": "量未指定" + }, + { + "name": "蒸肉粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸肉粉", + "notes": "量未指定" + }, + { + "name": "细辣椒粉(吃辣则加)", + "quantity": null, + "unit": null, + "text_quantity": "- 细辣椒粉(吃辣则加)", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "五花肉(可加可不加)", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉(可加可不加)", + "notes": "量未指定" + }, + { + "name": "姜蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水", + "notes": "量未指定" + }, + { + "name": "鸭子:1000g", + "quantity": null, + "unit": null, + "text_quantity": "- 鸭子:1000g", + "notes": "量未指定" + }, + { + "name": "糯米粉:100g", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米粉:100g", + "notes": "量未指定" + }, + { + "name": "粘米粉:", + "quantity": null, + "unit": null, + "text_quantity": "- 粘米粉: 300g", + "notes": "量未指定" + }, + { + "name": "蒸肉粉:", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸肉粉: 50g", + "notes": "量未指定" + }, + { + "name": "细辣椒粉:", + "quantity": null, + "unit": null, + "text_quantity": "- 细辣椒粉: 50g", + "notes": "量未指定" + }, + { + "name": "白胡椒粉:5g", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉:5g", + "notes": "量未指定" + }, + { + "name": "五花肉:50g", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉:50g", + "notes": "量未指定" + }, + { + "name": "姜蒜:20g", + "quantity": null, + "unit": null, + "text_quantity": "- 姜蒜:20g", + "notes": "量未指定" + }, + { + "name": "盐:10g", + "quantity": null, + "unit": null, + "text_quantity": "- 盐:10g", + "notes": "量未指定" + }, + { + "name": "食用油:10g", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油:10g", + "notes": "量未指定" + }, + { + "name": "开水:100g", + "quantity": null, + "unit": null, + "text_quantity": "- 开水:100g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将糯米粉、粘米粉、蒸肉粉、细辣椒粉、5 克盐、白胡椒粉倒一起搅匀" + }, + { + "step": 2, + "description": "鸭子让热心摊主剁成蒸煮块,姜切片,蒜子剥皮,五花肉切片即可" + }, + { + "step": 3, + "description": "热锅凉油煸炒五花肉出油,再加食用油烧热,下入鸭子煸炒" + }, + { + "step": 4, + "description": "鸭子煸炒到表皮焦变色,下入姜蒜和盐继续煸炒香味" + }, + { + "step": 5, + "description": "关小火倒入米粉翻炒,鸭肉均匀裹满米粉子,加入开水,少量多次的加,边加边翻炒" + }, + { + "step": 6, + "description": "翻炒鸭肉和米粉有湿感,铲出入碗中,高压锅放水蒸 20-25 分钟" + }, + { + "step": 7, + "description": "出锅前撒点葱花即可享用了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-牛排-牛排", + "name": "牛排的做法", + "description": "# 牛排的做法\n\n![牛排成品](./牛排.jpg)\n\n牛排是一种广受欢迎的西式肉类料理,富含蛋白质、油脂和铁、锌等矿物质。牛排的烹饪过程通过灵活的烹饪手法(如煎、烤、慢煮、熟成)控制牛排的熟度,从三分熟(中心为粉红色)到全熟(完全熟透)可选。高温烹饪能形成焦香外壳,搭配盐、大蒜、黄油、香料可以得到丰富的风味。牛排烹饪的入门较为简单,但精通困难。本文主要介绍最简单的煎牛排,全部烹饪过程耗时 15-30 分钟。图示为 5 分熟的牛小排(short ribs)。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/牛排/牛排.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/牛排/牛排.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/牛排/牛排.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "平底锅(有条件的推荐铸铁平底锅)", + "quantity": null, + "unit": null, + "text_quantity": "- 平底锅(有条件的推荐铸铁平底锅)", + "notes": "量未指定" + }, + { + "name": "锡箔纸(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 锡箔纸(可选)", + "notes": "量未指定" + }, + { + "name": "厨房纸(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 厨房纸(可选)", + "notes": "量未指定" + }, + { + "name": "汤匙", + "quantity": null, + "unit": null, + "text_quantity": "- 汤匙", + "notes": "量未指定" + }, + { + "name": "橄榄油(推荐特级初榨橄榄油)", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油(推荐特级初榨橄榄油)", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "盐(推荐大颗粒海盐)", + "quantity": null, + "unit": null, + "text_quantity": "- 盐(推荐大颗粒海盐)", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉(推荐粗颗粒现磨黑胡椒)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉(推荐粗颗粒现磨黑胡椒)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "香料(可选,推荐迷迭香或者百里香,尽量使用新鲜的植物枝条而不是这些香料磨成的粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 香料(可选,推荐迷迭香或者百里香,尽量使用新鲜的植物枝条而不是这些香料磨成的粉)", + "notes": "量未指定" + }, + { + "name": "预制牛排酱汁(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 预制牛排酱汁(可选)", + "notes": "量未指定" + }, + { + "name": "配菜( 可选,按喜好准备,这里推荐芦笋,口蘑,小番茄,小土豆,选其中的", + "quantity": null, + "unit": null, + "text_quantity": "- 配菜( 可选,按喜好准备,这里推荐芦笋,口蘑,小番茄,小土豆,选其中的 1-2 种即可)", + "notes": "量未指定" + }, + { + "name": "牛排", + "quantity": null, + "unit": null, + "text_quantity": "- 牛排 450-500g(两片牛排)", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉 2g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 1 个(约 25-30g,实际用量约为 5-10g)", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油 10-15ml", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 20-25g", + "notes": "量未指定" + }, + { + "name": "口蘑", + "quantity": null, + "unit": null, + "text_quantity": "- 口蘑 5-10 个", + "notes": "量未指定" + }, + { + "name": "小土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 小土豆 5-10 个(每个约 20g)", + "notes": "量未指定" + }, + { + "name": "小番茄", + "quantity": null, + "unit": null, + "text_quantity": "- 小番茄 5-10 个(每个约 15g)", + "notes": "量未指定" + }, + { + "name": "百里香", + "quantity": null, + "unit": null, + "text_quantity": "- 百里香 2g(若使用新鲜百里香枝条,取 3-6 根,每根约 10cm 长即可)", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-猪皮冻-猪皮冻", + "name": "猪皮冻的做法", + "description": "# 猪皮冻的做法\n\n![无骨鸡爪成品](./猪皮冻.jpg)\n\n猪皮冻是一道简单易做的菜。北方人年夜饭餐桌上的“常青树”,晶莹剔透的外表,滑嫩 Q 弹的口感,是不折不扣的超级下酒菜。\n\n猪皮美容养颜,难度稍高,预计制作时长 24 小时。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/猪皮冻/猪皮冻.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/猪皮冻/猪皮冻.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/猪皮冻/猪皮冻.jpg" + ], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪皮", + "quantity": null, + "unit": null, + "text_quantity": "- 猪皮", + "notes": "量未指定" + }, + { + "name": "大料、花椒、白芷、桂皮、丁香、香叶、小茴香", + "quantity": null, + "unit": null, + "text_quantity": "- 大料、花椒、白芷、桂皮、丁香、香叶、小茴香", + "notes": "量未指定" + }, + { + "name": "主料:猪皮", + "quantity": null, + "unit": null, + "text_quantity": "- 主料:猪皮 1kg、水 4kg", + "notes": "量未指定" + }, + { + "name": "香料包:八角", + "quantity": null, + "unit": null, + "text_quantity": "- 香料包:八角 10g、花椒 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将猪皮,剁成不超过 10cm 小块,用清水浸泡 12 小时,然后冷水下锅,加入姜 10g、料酒 50ml 后,汆烫 5-10 分钟,捞出放入冷水中" + }, + { + "step": 2, + "description": "将焯过水的猪皮,放到案板上,将里面的白肉部分全部剔除,然后再切成成不超过 3mm 的长条,放入盆中" + }, + { + "step": 3, + "description": "加入白醋 20g,盐 5g,用力搓洗 3 分钟,再用清水洗净,这时的猪皮已经基本没什么腥味" + }, + { + "step": 4, + "description": "锅内加入 4kg 水,放入猪皮,葱 10g,姜片 10g,八角 10g,花椒 5g,大火烧开后,小火煲煮 90 分钟至猪皮软烂" + }, + { + "step": 5, + "description": "再加入盐 8g、味精 10g、鸡精 15g、生抽 50ml、老抽 20ml 调味后,倒入盘中,将葱姜,八角拣出,晾凉至果冻状" + }, + { + "step": 6, + "description": "放冰箱冷藏即可,食用时,切成小块或者厚片" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-瘦肉土豆片-瘦肉土豆片", + "name": "瘦肉土豆片的做法", + "description": "# 瘦肉土豆片的做法\n\n![示例菜成品](./瘦肉土豆片.jpg)\n\n瘦肉土豆片是一道简单易做的菜。小炒家常菜,方便快速,适合上班族用于带饭必备小炒菜。一般初学者只需要 1 小时即可完成。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/瘦肉土豆片/瘦肉土豆片.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/瘦肉土豆片/瘦肉土豆片.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/瘦肉土豆片/瘦肉土豆片.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "纯瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 纯瘦肉", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗", + "notes": "量未指定" + }, + { + "name": "生粉(玉米淀粉或其他淀粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉(玉米淀粉或其他淀粉)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "纯瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 纯瘦肉 200g", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 200g", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗 2 根(约 20 g)", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 3g", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "土豆去皮、对半切开,再切成约 2mm 的薄片,备用" + }, + { + "step": 2, + "description": "蒜苗洗净,切成约 1cm 的段,备用" + }, + { + "step": 3, + "description": "瘦肉洗净切成约 2mm 的薄片,放入碗中,加入 5g 生粉、5g 生抽、3g 老抽腌制十分钟,备用" + }, + { + "step": 4, + "description": "瘦肉腌制时,烧一锅开水,将土豆片放入锅中,焯水,约 5 分钟" + }, + { + "step": 5, + "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高" + }, + { + "step": 6, + "description": "放入瘦肉,翻炒至变色,倒入蒜苗一起炒,蒜苗炒约 20 秒" + }, + { + "step": 7, + "description": "放入土豆,保持翻炒,加入 2g 食用盐、5g 生抽," + }, + { + "step": 8, + "description": "炒约 3 分钟,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-糖醋排骨-糖醋排骨", + "name": "糖醋排骨的做法", + "description": "# 糖醋排骨的做法\n\n糖醋排骨是一道具有代表性的传统名菜,以其独特的酸甜口味深受大众喜爱。本菜谱在保留原有风味的基础上,对用料绑定、火候控制以及操作细节作了优化,旨在提高菜谱的可迁移性和可执行性。\n\n![示例菜成品](./1.jpeg)\n![示例菜成品](./2.jpeg)\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/糖醋排骨/糖醋排骨.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/糖醋排骨/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/糖醋排骨/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/糖醋排骨/2.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨 300 g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 30 g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5 ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5 ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 2 g", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片 2 片", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻 2 g", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 10 g", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 5 ml", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 2 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "排骨与姜片放入冷水中,大火加热至水沸腾、出现大量泡沫后转中火,待水持续沸腾时再转小火焯水 2–3 分钟,捞出备用。" + }, + { + "step": 2, + "description": "用开水反复清洗排骨 2–3 遍,确保彻底去除血沫。" + }, + { + "step": 3, + "description": "在锅中倒入足够量的食用油进行深炸(油量依据锅具大小而定,建议约 300 ml 供一般家庭使用),待油温升至约 170°C 后,下排骨炸制 3–5 分钟,直至表面略呈金黄色,捞出控油。" + }, + { + "step": 4, + "description": "另取干净锅,置于小火上加热 50 ml 热水,加入白砂糖 30 g,轻轻搅拌直至糖完全溶解,并略呈淡黄色。此步骤的重点在于观察糖溶解情况,无需过分依赖颜色变化。" + }, + { + "step": 5, + "description": "将炸好的排骨倒入炒制糖水的锅中,迅速翻炒 30 秒后,依次加入香醋 5 ml、生抽 5 ml、蚝油 5 ml、鸡精 2 g、番茄酱 10 g、五香粉 2 g,再次翻炒 30 秒,使调料均匀裹覆排骨,然后加入开水至刚好没过排骨。" + }, + { + "step": 6, + "description": "用大火将锅中液体煮沸后,加入老抽 5 ml 进行上色,并快速收汁;若排骨块较大,可转小火焖煮 5–10 分钟以便更好地入味,切勿采用中火长时间炖煮 20 分钟,以免损伤口感。" + }, + { + "step": 7, + "description": "起锅装盘,撒上芝麻 2 g,即可享用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-红烧猪蹄-红烧猪蹄", + "name": "红烧猪蹄的做法", + "description": "# 红烧猪蹄的做法\n\n![红烧猪蹄](./红烧猪蹄.jpg)\n\n红烧猪蹄营养丰富,味道香,汤汁浓郁、下饭强。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/红烧猪蹄/红烧猪蹄.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧猪蹄/红烧猪蹄.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧猪蹄/红烧猪蹄.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪蹄", + "quantity": null, + "unit": null, + "text_quantity": "- 猪蹄", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "猪蹄:2~3 根", + "quantity": null, + "unit": null, + "text_quantity": "- 猪蹄:2~3 根", + "notes": "量未指定" + }, + { + "name": "食用油:30ml", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油:30ml", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 2 片", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 5 片", + "notes": "量未指定" + }, + { + "name": "葱半根", + "quantity": null, + "unit": null, + "text_quantity": "- 葱半根", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 1 块", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 7-8 粒", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 30 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 20 ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 20 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 8 克", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 4 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "冷水锅中放入热心摊主剁好的猪蹄,加入 20 ml 料酒与葱姜,煮 15 分钟去掉血腥" + }, + { + "step": 2, + "description": "热锅冷油,倒入 30ml 食用油,放入 7-8 粒冰糖,开小火,熬成糖色,期间用锅铲把冰糖压碎,大概熬 2 分钟" + }, + { + "step": 3, + "description": "熬成糖色后,放入焯过水的猪蹄,继续小火,翻炒猪蹄,直至所有猪蹄两面微黄" + }, + { + "step": 4, + "description": "加入香叶 2 片、桂皮 1 块、八角 4 个、生抽 20 ml、老抽 20 ml、料酒 10 ml、姜 3 片、盐 8 克,转中火、继续翻炒 1 分钟" + }, + { + "step": 5, + "description": "加入开水或者冷水,水需要没过猪蹄,盖上锅盖,大火烧开,烧开之后关火" + }, + { + "step": 6, + "description": "把锅内的食材全部倒入高压锅中,高压锅中需要 15 分钟(如果同学没有高压锅,可放在锅中大火转小火熬制即可)" + }, + { + "step": 7, + "description": "15 分钟之后,把高压锅的食材倒入炒锅中,开大火收汁,此时可用筷子尝下味道、淡的话可以加 2~3g 盐" + }, + { + "step": 8, + "description": "大火收汁时长根据锅中的水来算,一般 30 秒即可,多留点也无碍、红烧猪蹄汤也是很下饭的" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-红烧肉-南派红烧肉", + "name": "南派红烧肉的做法", + "description": "# 南派红烧肉的做法\n\n这份红烧肉教程是一道新手不败的菜谱。配着米饭好吃的停不下来,香糯无敌棒色泽诱人肥而不腻\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/红烧肉/南派红烧肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/001.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "注:如果有可能,请尽量把刀磨的锋利一些。", + "quantity": null, + "unit": null, + "text_quantity": "- 注:如果有可能,请尽量把刀磨的锋利一些。", + "notes": "量未指定" + }, + { + "name": "工具:`锅`(砂锅为宜,铝锅其次,高压锅也可以,最好不要铁锅、铜锅)", + "quantity": null, + "unit": null, + "text_quantity": "- 工具:`锅`(砂锅为宜,铝锅其次,高压锅也可以,最好不要铁锅、铜锅)", + "notes": "量未指定" + }, + { + "name": "主料:`五花肉`", + "quantity": null, + "unit": null, + "text_quantity": "- 主料:`五花肉`", + "notes": "量未指定" + }, + { + "name": "猪五花肉:约", + "quantity": null, + "unit": null, + "text_quantity": "- 猪五花肉:约 2 斤", + "notes": "量未指定" + }, + { + "name": "油:100-150ml,色拉油、猪油、花生油都可以", + "quantity": null, + "unit": null, + "text_quantity": "- 油:100-150ml,色拉油、猪油、花生油都可以", + "notes": "量未指定" + }, + { + "name": "姜:", + "quantity": null, + "unit": null, + "text_quantity": "- 姜: 6 片", + "notes": "量未指定" + }, + { + "name": "冰糖:约", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖:约 15 块", + "notes": "量未指定" + }, + { + "name": "白砂糖:30g", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖:30g", + "notes": "量未指定" + }, + { + "name": "老抽:15ml", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽:15ml", + "notes": "量未指定" + }, + { + "name": "料酒:20ml", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒:20ml", + "notes": "量未指定" + }, + { + "name": "凉水:没过食材的量即可,看锅大小准备", + "quantity": null, + "unit": null, + "text_quantity": "- 凉水:没过食材的量即可,看锅大小准备", + "notes": "量未指定" + }, + { + "name": "开水:没过食材的量即可,看锅大小准备", + "quantity": null, + "unit": null, + "text_quantity": "- 开水:没过食材的量即可,看锅大小准备", + "notes": "量未指定" + }, + { + "name": "香叶:4 片", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶:4 片", + "notes": "量未指定" + }, + { + "name": "八角:3 个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角:3 个", + "notes": "量未指定" + }, + { + "name": "盐:2-3g", + "quantity": null, + "unit": null, + "text_quantity": "- 盐:2-3g", + "notes": "量未指定" + }, + { + "name": "花椒:10g", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒:10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "`猪五花肉`切大块(约 4.5cm )" + }, + { + "step": 2, + "description": "`生姜`切片(每片厚度约 3mm )" + }, + { + "step": 3, + "description": "`开水`烧开" + }, + { + "step": 4, + "description": "`凉水`自来水即可" + }, + { + "step": 5, + "description": "`小葱`小葱白色的部分`葱白`切成小段(小葱最佳,大葱也可以)" + }, + { + "step": 6, + "description": "`蒜`中间切开,不要拍扁,否则难以捞出以至最后`收汁`时影响味道" + }, + { + "step": 7, + "description": "建议先拿出来一半葱姜,再将剩下的`生姜、葱白、蒜、花椒、八角、香叶`提前放入一个碗中备用" + }, + { + "step": 8, + "description": "凉水锅中放入切好的五花肉,加入料酒与 2/5 葱姜,煮 15 分钟去掉血腥,捞出来后洗干净;" + }, + { + "step": 9, + "description": "炒[糖色](./../../condiment/简易版炒糖色.md),注意采用其中提到的操作 2 来制作糖色。" + }, + { + "step": 10, + "description": "将准备好的`生姜、葱白、蒜、花椒、八角、香叶`还有`五花肉`倒入锅中`大火`翻炒,期间加入至闻到香味,倒入开水至没过全部肉炖煮 50 分钟-60 分钟" + }, + { + "step": 11, + "description": "加入 10ml 料酒;" + }, + { + "step": 12, + "description": "盖上锅盖煮至沸腾后,每隔 25 分钟打开盖子将浮在表面的油和沫捞出;" + }, + { + "step": 13, + "description": "当水的高度减至肉最高的高度与锅底高度的 3/5 时,转中火,并捞出除肉和水以外的所有辅料,开始收汁;" + }, + { + "step": 14, + "description": "打开锅盖,待汤汁快没有的时粘稠状出锅(切记不可收干);" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-红烧肉-简易红烧肉", + "name": "简易红烧肉的做法", + "description": "# 简易红烧肉的做法\n\n这份红烧肉教程是一道新手不败的菜谱。配着米饭好吃的停不下来,香糯无敌棒色泽诱人肥而不腻。建议搭配米饭食用。\n\n![红烧肉成品](./000.jpg)\n\n![红烧肉成品](./001.jpg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/红烧肉/简易红烧肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/001.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "注:如果有可能,请尽量把刀磨的锋利一些。", + "quantity": null, + "unit": null, + "text_quantity": "- 注:如果有可能,请尽量把刀磨的锋利一些。", + "notes": "量未指定" + }, + { + "name": "主料:`大肉`、`鸡蛋`(可选)、`豆皮`(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 主料:`大肉`、`鸡蛋`(可选)、`豆皮`(可选)", + "notes": "量未指定" + }, + { + "name": "猪五花肉:约", + "quantity": null, + "unit": null, + "text_quantity": "- 猪五花肉:约 3~4 斤", + "notes": "量未指定" + }, + { + "name": "姜:", + "quantity": null, + "unit": null, + "text_quantity": "- 姜: 6 片", + "notes": "量未指定" + }, + { + "name": "冰糖:15 克(约", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖:15 克(约 7 块)", + "notes": "量未指定" + }, + { + "name": "生抽:10ml", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽:10ml", + "notes": "量未指定" + }, + { + "name": "老抽:15ml", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽:15ml", + "notes": "量未指定" + }, + { + "name": "料酒:5ml", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒:5ml", + "notes": "量未指定" + }, + { + "name": "开水:没过食材的量,需要", + "quantity": null, + "unit": null, + "text_quantity": "- 开水:没过食材的量,需要 600ml-900ml", + "notes": "量未指定" + }, + { + "name": "香叶:3 片", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶:3 片", + "notes": "量未指定" + }, + { + "name": "八角:2 个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角:2 个", + "notes": "量未指定" + }, + { + "name": "鹌鹑蛋(可选,没有鹌鹑蛋,可以用同等重量的鸡蛋代替):0-2 个", + "quantity": null, + "unit": null, + "text_quantity": "- 鹌鹑蛋(可选,没有鹌鹑蛋,可以用同等重量的鸡蛋代替):0-2 个", + "notes": "量未指定" + }, + { + "name": "豆皮(可选):0-80g", + "quantity": null, + "unit": null, + "text_quantity": "- 豆皮(可选):0-80g", + "notes": "量未指定" + }, + { + "name": "盐:2-3g", + "quantity": null, + "unit": null, + "text_quantity": "- 盐:2-3g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "`猪五花肉`切大块(约 4.5cm ,冷冻半小时至一小时更好切)" + }, + { + "step": 2, + "description": "`豆皮`切 2cm 的宽度" + }, + { + "step": 3, + "description": "`生姜`切片(每片厚度约 3mm )" + }, + { + "step": 4, + "description": "`水`烧开" + }, + { + "step": 5, + "description": "`鹌鹑蛋`煮熟并用`叉子`/`牙签`扎孔(尽量多些好入味)" + }, + { + "step": 6, + "description": "`大葱`大葱白色的部分`葱白`" + }, + { + "step": 7, + "description": "冷水锅中放入切好的`猪五花肉`,加入料酒与葱姜,煮 15 分钟去掉血腥" + }, + { + "step": 8, + "description": "锅中放入两片`生姜`提味" + }, + { + "step": 9, + "description": "开中小火后直接加入`五花肉`,不需要放入食用油,每块`五花肉`六个面都煎一下,煎至出油即可" + }, + { + "step": 10, + "description": "将煎出的油倒出备用,并将`五花肉`推至一边,加入 15g `冰糖`,翻炒至`冰糖`融化;" + }, + { + "step": 11, + "description": "融化后将五花肉与冰糖炒至融合上色,加入" + }, + { + "step": 12, + "description": "加入`烧好的开水`炖煮 40 分钟(刀工差的同学切的过大请自觉延长炖煮时间),并放入" + }, + { + "step": 13, + "description": "盖上锅盖煮至沸腾后,加入煮好扎好孔的`鹌鹑蛋`和`豆皮`,开中小火,等待 40 分钟。(中途可适当翻搅防止粘锅);" + }, + { + "step": 14, + "description": "打开锅盖,待汤汁快没有的时候开大火收汁(切记不可收干);" + }, + { + "step": 15, + "description": "加入 2-3g `盐`,翻炒一下,就可以出锅了。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-羊排焖面-羊排焖面", + "name": "羊排焖面的做法", + "description": "# 羊排焖面的做法\n\n![羊排焖面](./羊排焖面.jpg)\n羊排焖面是一道硬菜,适合聚会时候大展身手。缺点就是有点花时间,优点就是好吃,而且一道菜补齐人体所需的三大营养物质。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/羊排焖面/羊排焖面.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/羊排焖面/羊排焖面.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/羊排焖面/羊排焖面.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "带皮羊排肉", + "quantity": null, + "unit": null, + "text_quantity": "- 带皮羊排肉", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "甜椒", + "quantity": null, + "unit": null, + "text_quantity": "- 甜椒", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "带皮羊排", + "quantity": null, + "unit": null, + "text_quantity": "- 带皮羊排 500g", + "notes": "量未指定" + }, + { + "name": "青椒,甜椒 各", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒,甜椒 各 2 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "羊肉冷水下锅焯水,水开了之后把血沫撇掉,捞出羊肉。" + }, + { + "step": 2, + "description": "切好生姜( 4 片),放入干辣椒与花椒在碗里备用。" + }, + { + "step": 3, + "description": "在炒锅加入油。(多一点也没关系)" + }, + { + "step": 4, + "description": "油热之后,放入白砂糖,给羊肉炒出焦糖色。" + }, + { + "step": 5, + "description": "羊肉水份炒干之后,放入盐、老抽,以及备好的调味料。" + }, + { + "step": 6, + "description": "加入清水没过羊肉,大火煮沸之后,让其继续煮 10 分钟,之后小火炖煮 30 分钟。" + }, + { + "step": 7, + "description": "在此期间,可以和面。和面的量以及操作方法在附加内容里讲解 *(注 1)。" + }, + { + "step": 8, + "description": "放入青椒,甜椒,大葱,以及面皮进行翻炒。" + }, + { + "step": 9, + "description": "翻炒均匀之后,即可出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-老妈蹄花-老妈蹄花", + "name": "老妈蹄花的做法", + "description": "# 老妈蹄花的做法\n\n![result 3](./result3.jpg)\n\n红烧猪蹄营养丰富,口感细腻,软烂脱骨,配上酸辣汁简直太香!\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/老妈蹄花/老妈蹄花.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result2.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result3.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪蹄(尽量选择猪前蹄:肉多筋多骨头少)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪蹄(尽量选择猪前蹄:肉多筋多骨头少)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "白芷", + "quantity": null, + "unit": null, + "text_quantity": "- 白芷", + "notes": "量未指定" + }, + { + "name": "当归(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 当归(可选)", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "花椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒油", + "notes": "量未指定" + }, + { + "name": "油泼辣子(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 油泼辣子(可选)", + "notes": "量未指定" + }, + { + "name": "白芸豆(没有可用海带)", + "quantity": null, + "unit": null, + "text_quantity": "- 白芸豆(没有可用海带)", + "notes": "量未指定" + }, + { + "name": "猪蹄:3 根", + "quantity": null, + "unit": null, + "text_quantity": "- 猪蹄:3 根", + "notes": "量未指定" + }, + { + "name": "白芸豆:200g", + "quantity": null, + "unit": null, + "text_quantity": "- 白芸豆:200g", + "notes": "量未指定" + }, + { + "name": "当归:2g", + "quantity": null, + "unit": null, + "text_quantity": "- 当归:2g", + "notes": "量未指定" + }, + { + "name": "白胡椒粉:5g", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉:5g", + "notes": "量未指定" + }, + { + "name": "姜片:30g", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片:30g", + "notes": "量未指定" + }, + { + "name": "当归:2g", + "quantity": null, + "unit": null, + "text_quantity": "- 当归:2g", + "notes": "量未指定" + }, + { + "name": "蒜末:8g", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末:8g", + "notes": "量未指定" + }, + { + "name": "鸡精:2g", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精:2g", + "notes": "量未指定" + }, + { + "name": "生抽:25g", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽:25g", + "notes": "量未指定" + }, + { + "name": "葱花:10g", + "quantity": null, + "unit": null, + "text_quantity": "- 葱花:10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "200g 白芸豆提前一晚清水浸泡备用" + }, + { + "step": 2, + "description": "准备猪前蹄,买菜的时候让师傅从中间劈开,用喷火枪去除毛囊,拿回家清洗" + }, + { + "step": 3, + "description": "冷水锅中加入猪蹄、大葱段、姜片、料酒,焯水十分钟,撇去浮沫,捞出洗干净备用" + }, + { + "step": 4, + "description": "高压锅中放入猪蹄、当归、白芷、白胡椒粉、姜片,上汽后压三十分钟,放入白芸豆,再压十分钟,这个时候如果汤底是奶白色,那么恭喜是正确的(如果中途需要加水,只能加热水)" + }, + { + "step": 5, + "description": "揭盖后加入盐、鸡精、葱花调味" + }, + { + "step": 6, + "description": "调制灵魂汁子:放入葱、蒜、小米椒,白胡椒粉、生抽、香醋、油泼辣子、花椒油、猪蹄原汤" + }, + { + "step": 7, + "description": "灵魂汁子,浇给" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-老式锅包肉-老式锅包肉", + "name": "老式锅包肉的做法", + "description": "# 老式锅包肉的做法\n\n锅包肉是东北名菜,创始于光绪年间哈尔滨道台府厨师郑兴文之手。老式锅包肉的酸味来源于白醋汁,口味酸甜酥脆。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/老式锅包肉/老式锅包肉.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪通脊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪通脊肉", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "胡萝卜(可无)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜(可无)", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "白醋(建议使用", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋(建议使用 9 度的醋,这样才会有较为突出的老式锅包肉特有的醋香)", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "土豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆淀粉", + "notes": "量未指定" + }, + { + "name": "中筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 中筋面粉", + "notes": "量未指定" + }, + { + "name": "小苏打", + "quantity": null, + "unit": null, + "text_quantity": "- 小苏打", + "notes": "量未指定" + }, + { + "name": "白熟芝麻(可无)", + "quantity": null, + "unit": null, + "text_quantity": "- 白熟芝麻(可无)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "猪通脊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪通脊肉 300g", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 50g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 30g", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 3-4 瓣", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 10g(可无)", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 10g", + "notes": "量未指定" + }, + { + "name": "白熟芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白熟芝麻 5g(可无)", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋 40g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 40g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 20ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 8g", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精 5g", + "notes": "量未指定" + }, + { + "name": "米醋", + "quantity": null, + "unit": null, + "text_quantity": "- 米醋 5ml(可无)", + "notes": "量未指定" + }, + { + "name": "土豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆淀粉 210g", + "notes": "量未指定" + }, + { + "name": "中筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 中筋面粉 70g", + "notes": "量未指定" + }, + { + "name": "小苏打", + "quantity": null, + "unit": null, + "text_quantity": "- 小苏打 5g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 1000ml(用于炸制)", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-芥末罗氏虾-芥末罗氏虾", + "name": "芥末罗氏虾的做法", + "description": "# 芥末罗氏虾的做法\n\n![芥末罗氏虾成品](./芥末罗氏虾成品.jpg)\n\n本菜品可替换成任意虾种类,包括但不限于基围虾、花虾、黑虎虾等。鲜香入味、芥末风味十足、吃完吮指,且操作十分简单。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/芥末罗氏虾/芥末罗氏虾.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/芥末罗氏虾/芥末罗氏虾成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/芥末罗氏虾/芥末罗氏虾成品.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "罗氏虾", + "quantity": null, + "unit": null, + "text_quantity": "- 罗氏虾", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "青芥末", + "quantity": null, + "unit": null, + "text_quantity": "- 青芥末", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "小米辣(不吃辣可不放或替换成红菜椒)", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣(不吃辣可不放或替换成红菜椒)", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "罗氏虾", + "quantity": null, + "unit": null, + "text_quantity": "- 罗氏虾 250g", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 1-2 个", + "notes": "量未指定" + }, + { + "name": "青芥末", + "quantity": null, + "unit": null, + "text_quantity": "- 青芥末 20g,用于碗汁", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉 10g,用于碗汁", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 30g,用于碗汁", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 5g,用于碗汁", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 3g,用于碗汁", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 15g,用于碗汁", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g,用于碗汁", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 1-2 个", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 20g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 80ml", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-茭白炒肉-茭白炒肉", + "name": "茭白炒肉的做法", + "description": "# 茭白炒肉的做法\n\n茭白味道鲜美,有一定营养价值\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/茭白炒肉/茭白炒肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/茭白炒肉/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/茭白炒肉/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/茭白炒肉/2.jpeg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "茭白", + "quantity": null, + "unit": null, + "text_quantity": "- 茭白", + "notes": "量未指定" + }, + { + "name": "瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 瘦肉", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "茭白", + "quantity": null, + "unit": null, + "text_quantity": "- 茭白 2 根", + "notes": "量未指定" + }, + { + "name": "瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 瘦肉 100 g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 15 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 30 ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 5 g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 1 片", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 1 个", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 5 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "茭白切片,每片厚度 0.5 cm" + }, + { + "step": 2, + "description": "瘦肉切条,厚度 0.3-0.5 cm,加入料酒、生粉、盐、水搅拌" + }, + { + "step": 3, + "description": "姜切片、蒜头剁碎" + }, + { + "step": 4, + "description": "起锅水烧开,放入茭白,水煮 60-90 S 后取出沥干" + }, + { + "step": 5, + "description": "起锅,倒入 15 ml 油,倒入瘦肉,反复翻炒 60 S 取出" + }, + { + "step": 6, + "description": "起锅,倒入 15 ml 油,倒入姜、蒜翻炒 30S,加入茭白继续翻炒 30 S" + }, + { + "step": 7, + "description": "继续加入瘦肉翻炒 60 S,加入 20 ml 水,加入盐、鸡精后翻炒 60S 出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-荔枝肉-荔枝肉", + "name": "荔枝肉的做法", + "description": "# 荔枝肉的做法\n\n荔枝肉独具闽菜特点,味道酸甜可口。是福州地区比较常见的一道菜肴\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/荔枝肉/荔枝肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/3.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/4.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/5.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/6.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/7.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 瘦肉", + "notes": "量未指定" + }, + { + "name": "凤梨", + "quantity": null, + "unit": null, + "text_quantity": "- 凤梨", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "鲜香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜香菇 2 朵", + "notes": "量未指定" + }, + { + "name": "蟹味菇", + "quantity": null, + "unit": null, + "text_quantity": "- 蟹味菇 30 g", + "notes": "量未指定" + }, + { + "name": "瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 瘦肉 150 g", + "notes": "量未指定" + }, + { + "name": "凤梨", + "quantity": null, + "unit": null, + "text_quantity": "- 凤梨 100 g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 500 ml", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 5 g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 100 g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 5 g", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末 5 g", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻 2 g", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 20 g", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 2 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "瘦肉切块(每块 2-3 cm ),放入大碗中,加入鸡蛋 1 个 、生粉 50 g 、生抽 3 ml 、鸡精 2 g" + }, + { + "step": 2, + "description": "充分搅拌,直至生粉包裹住瘦肉块(太稀则继续加生粉,太干则加水),然后加入 5 ml 油,在充分搅拌后备用" + }, + { + "step": 3, + "description": "在准备一个碗,加入番茄酱、鸡精 3 g 、生抽 2 ml 、姜末、白砂糖、生粉 10 g \\香醋、凉水 200 ml ,充分搅拌后备用" + }, + { + "step": 4, + "description": "切一个凤梨, 准备 6 个 (每个 1.5-2 cm)凤梨块" + }, + { + "step": 5, + "description": "起锅烧油,倒入 500 ml 油,一直烧油直到听到油炸声" + }, + { + "step": 6, + "description": "将瘦肉一个一个放入锅中(切记不可以整碗倒入),保证每个肉不要粘在一起" + }, + { + "step": 7, + "description": "全部放入瘦肉后,每 30 S 用勺子来回两面翻转瘦肉块,直至瘦肉块表面金黄" + }, + { + "step": 8, + "description": "取出瘦肉,一分钟后倒入油锅中继续炸,直至瘦肉块表面出现焦黄后,取出放入大碗备用" + }, + { + "step": 9, + "description": "起锅,倒入汤汁,30 S 后倒入瘦肉块、凤梨块,充分翻炒后 出锅" + }, + { + "step": 10, + "description": "摆上芝麻" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-荷兰豆炒腊肠-荷兰豆炒腊肠", + "name": "荷兰豆炒腊肠的做法", + "description": "# 荷兰豆炒腊肠的做法\n\n![荷兰豆炒腊肠](./1.png)\n\n荷兰豆炒腊肠是一道营养丰富,口感清爽,有利于开胃助食,增进食欲的美味菜肴。荷兰豆中富含人体所需的各种营养物质,尤其是含有优质蛋白质,可以提高机体的抗病能力和康复能力。\n\n预估烹饪难度:★★", + "source_path": "dishes/meat_dish/荷兰豆炒腊肠/荷兰豆炒腊肠.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荷兰豆炒腊肠/1.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荷兰豆炒腊肠/1.png", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荷兰豆炒腊肠/2.png" + ], + "category": "荤菜", + "difficulty": 2, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "荷兰豆", + "quantity": null, + "unit": null, + "text_quantity": "- 荷兰豆", + "notes": "量未指定" + }, + { + "name": "腊肠", + "quantity": null, + "unit": null, + "text_quantity": "- 腊肠", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "荷兰豆大约", + "quantity": null, + "unit": null, + "text_quantity": "- 荷兰豆大约 50 个", + "notes": "量未指定" + }, + { + "name": "腊肠约", + "quantity": null, + "unit": null, + "text_quantity": "- 腊肠约 100 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "把荷兰豆去掉蒂,有时间的也可以同时把尾部去掉" + }, + { + "step": 2, + "description": "买腊肠之前可以问老板是生的还是熟的,如果是生的,需要提前蒸一下,如果是熟的可以直接使用" + }, + { + "step": 3, + "description": "把荷兰豆清洗一下,然后焯一下水,大概 45s,荷兰豆焯至变色即可,捞出过凉水备用" + }, + { + "step": 4, + "description": "热锅,锅内放入大约 10ml 食用油。等待 10 秒让油温升高" + }, + { + "step": 5, + "description": "放入腊肠,保持翻炒至腊肠*微微卷边*,注意这里一定要**保持小火**,小到不能小的那种,不然容易糊" + }, + { + "step": 6, + "description": "放入荷兰豆,转为中大火,翻炒 30s 放入生抽,接着再翻炒 20-30s 即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-血浆鸭-血浆鸭", + "name": "血浆鸭的做法", + "description": "# 血浆鸭的做法\n\n![血浆鸭(特辣)](./血浆鸭(特辣).jpg)\n\n![血浆鸭(微辣)](./血浆鸭(微辣).jpg)\n\n血浆鸭是湖南武冈特色传统名菜,香、脆可口,由于醋血的作用,不仅鸭骨酥而脆,就是姜和辣椒也变得不辣而甜净。一般初学者只需要 2 小时就可以完成。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/血浆鸭/血浆鸭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/血浆鸭/血浆鸭(微辣).jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/血浆鸭/血浆鸭(微辣).jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/血浆鸭/血浆鸭(特辣).jpg" + ], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鲜仔鸭肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜仔鸭肉", + "notes": "量未指定" + }, + { + "name": "鲜鸭血(宰杀鸭子时加醋接鸭血,用筷子顺时针搅拌防凝固)", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜鸭血(宰杀鸭子时加醋接鸭血,用筷子顺时针搅拌防凝固)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜仔", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜仔", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒", + "notes": "量未指定" + }, + { + "name": "酒(或者白酒、啤酒、米酒皆可)", + "quantity": null, + "unit": null, + "text_quantity": "- 酒(或者白酒、啤酒、米酒皆可)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "鲜仔鸭肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜仔鸭肉 2000g", + "notes": "量未指定" + }, + { + "name": "鲜鸭血", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜鸭血 250ml", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 6 片 (根据个人吃辣喜好程度可多放 1-3 片姜)", + "notes": "量未指定" + }, + { + "name": "蒜仔", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜仔 6 瓣", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 2 根,切好备用", + "notes": "量未指定" + }, + { + "name": "酒(任选其一):", + "quantity": null, + "unit": null, + "text_quantity": "- 酒(任选其一):", + "notes": "量未指定" + }, + { + "name": "高度白酒", + "quantity": null, + "unit": null, + "text_quantity": "- 高度白酒 50ml + 水 150ml", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒 200ml", + "notes": "量未指定" + }, + { + "name": "米酒", + "quantity": null, + "unit": null, + "text_quantity": "- 米酒 200ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 30ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 8g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鲜仔鸭肉切成约 3cm 小块,加料酒、姜片,去除血水。" + }, + { + "step": 2, + "description": "炒锅烧热,放入约 100ml 食用油,大火待油烧开,放入腌制好的鲜鸭肉,不断翻炒。" + }, + { + "step": 3, + "description": "待鸭肉完全变色(肉眼可见泛白),放入酒,再加入 200ml 开水,刚好淹没鸭肉即可,盖上锅盖中火煮 15 分钟。" + }, + { + "step": 4, + "description": "水开之后,打开锅盖放入姜蒜,翻炒一遍,盖上锅盖持续加热 10 分钟。" + }, + { + "step": 5, + "description": "打开锅盖放入辣椒,不断翻炒,待至肉眼可见辣椒炒软,放入鲜鸭血,此时需要不断翻炒,确保每块鸭肉和每片辣椒都有鸭血的浸润(此乃血浆鸭的精髓)。" + }, + { + "step": 6, + "description": "翻炒至肉眼可见鸭血均为黑色,加入盐,鸡精,香葱,(喜欢食用山胡椒油的朋友也可以在此时放入 3-6 滴山胡椒油)再次翻炒一到二次即可。" + }, + { + "step": 7, + "description": "出锅盛盘,上桌食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-西红柿土豆炖牛肉-西红柿土豆炖牛肉", + "name": "西红柿土豆炖牛肉的做法", + "description": "# 西红柿土豆炖牛肉的做法\n\n![效果图](./abaaba_1.png)\n\n西红柿土豆炖牛肉(腩)的特点就是还挺好吃,牛肉是优质蛋白,换成牛腩更好吃。\n\n难度基本没有,90 岁老奶奶拄拐杖都能做。\n\n预计制作总时常 1~1.5h。炖的时间:做的时间≈3:1\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/西红柿土豆炖牛肉/西红柿土豆炖牛肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/西红柿土豆炖牛肉/abaaba_1.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/西红柿土豆炖牛肉/abaaba_1.png" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉", + "notes": "量未指定" + }, + { + "name": "小料", + "quantity": null, + "unit": null, + "text_quantity": "- 小料", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "白糖 or 冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 or 冰糖", + "notes": "量未指定" + }, + { + "name": "酱油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油(可选)", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉(或白胡椒粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉(或白胡椒粉)", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉 500-700g", + "notes": "量未指定" + }, + { + "name": "小料", + "quantity": null, + "unit": null, + "text_quantity": "- 小料", + "notes": "量未指定" + }, + { + "name": "葱一根,姜四片,料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 葱一根,姜四片,料酒", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 3g", + "notes": "量未指定" + }, + { + "name": "八角一个(半)", + "quantity": null, + "unit": null, + "text_quantity": "- 八角一个(半)", + "notes": "量未指定" + }, + { + "name": "香叶两片", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶两片", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 15ml (若用牛腩可根据喜好减少为 10ml)", + "notes": "量未指定" + }, + { + "name": "调味品", + "quantity": null, + "unit": null, + "text_quantity": "- 调味品", + "notes": "量未指定" + }, + { + "name": "白糖 or 冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 or 冰糖", + "notes": "量未指定" + }, + { + "name": "酱油(千禾酿造生抽无添加),老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油(千禾酿造生抽无添加),老抽", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉(白的也行)2g", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉(白的也行)2g", + "notes": "量未指定" + }, + { + "name": "土豆两三个(看喜好,锅能盛了为准)", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆两三个(看喜好,锅能盛了为准)", + "notes": "量未指定" + }, + { + "name": "西红柿拳头大小中等个头两三个", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿拳头大小中等个头两三个", + "notes": "量未指定" + }, + { + "name": "比拳头大一点的洋葱一个", + "quantity": null, + "unit": null, + "text_quantity": "- 比拳头大一点的洋葱一个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "备菜:" + }, + { + "step": 2, + "description": "制作" + }, + { + "step": 3, + "description": "炖煮" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-西红柿牛腩-西红柿牛腩", + "name": "西红柿牛腩的做法", + "description": "# 西红柿牛腩的做法\n\n西红柿牛腩汤汁浓厚酸甜可口,牛肉软绵醇香,搭配米饭绝配。一般初学者需要 90 分钟完成。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/西红柿牛腩/西红柿牛腩.md", + "image_path": null, + "images": [], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "牛腩", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腩", + "notes": "量未指定" + }, + { + "name": "燃气灶(西红柿去皮用)", + "quantity": null, + "unit": null, + "text_quantity": "- 燃气灶(西红柿去皮用)", + "notes": "量未指定" + }, + { + "name": "高压锅/砂锅/普通铝锅(铁锅)", + "quantity": null, + "unit": null, + "text_quantity": "- 高压锅/砂锅/普通铝锅(铁锅)", + "notes": "量未指定" + }, + { + "name": "2cm 两段葱段、两片姜片,葱花、姜各", + "quantity": null, + "unit": null, + "text_quantity": "- 2cm 两段葱段、两片姜片,葱花、姜各 10g", + "notes": "量未指定" + }, + { + "name": "生抽、白胡椒粉,白糖,料/黄酒,八角三小片", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽、白胡椒粉,白糖,料/黄酒,八角三小片", + "notes": "量未指定" + }, + { + "name": "牛腩(挑选肥瘦相间的口感比较好)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腩(挑选肥瘦相间的口感比较好)", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 3-4 个(每个约 200g)", + "notes": "量未指定" + }, + { + "name": "牛腩", + "quantity": null, + "unit": null, + "text_quantity": "- 牛腩 500g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 20-30ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "牛腩切条、切块成长宽高均 2cm ,冷水下锅,开锅煮制 2 分钟去除血水,捞出冲洗干净" + }, + { + "step": 2, + "description": "另起锅 2L 水烧开,加入 2cm 两段葱段、两片姜片、八角、料/黄酒 5-10ml,放入焯好的牛肉,盖盖炖制(砂锅 1 小时,高压锅炖肉模式 45 分钟),筷子能轻松插透就证明炖好了" + }, + { + "step": 3, + "description": "西红柿去皮:西红柿头部滑十字至腰线,筷子/刀叉从果蒂捅入,煤气灶小火,一边转动一边烤,及时拿下来查看,起皮后撕下来,切块。越小越好" + }, + { + "step": 4, + "description": "起锅烧油,油温 7 成热,葱、姜各 10g,番茄下锅,炒透炒出番茄红色,加入煮好的牛腩和原汤,原汤刚刚没过牛肉即可" + }, + { + "step": 5, + "description": "根据个人口味放入盐、糖、生抽调味盖盖" + }, + { + "step": 6, + "description": "开锅后大火继续炒制 3-5 分钟" + }, + { + "step": 7, + "description": "待番茄汁呈中等粘稠程度后关火,散入葱花,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-豆豉鲮鱼油麦菜-豆豉鲮鱼油麦菜", + "name": "豆豉鲮鱼油麦菜的做法", + "description": "# 豆豉鲮鱼油麦菜的做法\n\n![豆豉鲮鱼油麦菜](./豆豉鲮鱼油麦菜成品.jpg)\n\n豆豉鲮鱼油麦菜是一到十分常见的菜,材料简单,操作方便,鲮鱼咸香,非常下饭。\n\n预估烹饪难度:★★", + "source_path": "dishes/meat_dish/豆豉鲮鱼油麦菜/豆豉鲮鱼油麦菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豆豉鲮鱼油麦菜/豆豉鲮鱼油麦菜成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豆豉鲮鱼油麦菜/豆豉鲮鱼油麦菜成品.jpg" + ], + "category": "荤菜", + "difficulty": 2, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "油麦菜", + "quantity": null, + "unit": null, + "text_quantity": "- 油麦菜", + "notes": "量未指定" + }, + { + "name": "甘竹牌鲮鱼罐头", + "quantity": null, + "unit": null, + "text_quantity": "- 甘竹牌鲮鱼罐头", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "油麦菜", + "quantity": null, + "unit": null, + "text_quantity": "- 油麦菜 500g", + "notes": "量未指定" + }, + { + "name": "鲮鱼罐头", + "quantity": null, + "unit": null, + "text_quantity": "- 鲮鱼罐头 1 罐(250g 上下)", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 4 瓣", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 20g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 3g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15ml", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-豉汁蒸白鱔-豉汁蒸白鱔食谱", + "name": "豉汁蒸白鱔食谱的做法", + "description": "# 豉汁蒸白鱔的做法\n\n![豉汁蒸白鱔示例菜成品](./豉汁蒸白鱔.jpeg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/豉汁蒸白鱔/豉汁蒸白鱔食谱.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豉汁蒸白鱔/豉汁蒸白鱔.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豉汁蒸白鱔/豉汁蒸白鱔.jpeg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "白鱔(白鳝)", + "quantity": null, + "unit": null, + "text_quantity": "- 白鱔(白鳝)", + "notes": "量未指定" + }, + { + "name": "豆豉", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油", + "notes": "量未指定" + }, + { + "name": "生粉(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉(可选)", + "notes": "量未指定" + }, + { + "name": "红椒(可选,装饰用)", + "quantity": null, + "unit": null, + "text_quantity": "- 红椒(可选,装饰用)", + "notes": "量未指定" + }, + { + "name": "白鱔", + "quantity": null, + "unit": null, + "text_quantity": "- 白鱔 250g (约一条小白鱔,已去内脏并切成段)", + "notes": "量未指定" + }, + { + "name": "豆豉", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉 1 汤匙", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 2 瓣 (剁碎)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 3 片 (切丝)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1 根 (切段或丝)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 1.5 汤匙", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 0.5 茶匙", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 0.5 茶匙", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油 0.5 茶匙", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉 1 茶匙 (可选,用于腌制)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 1 汤匙 (调酱汁)", + "notes": "量未指定" + }, + { + "name": "红椒 少许 (切丝,装饰用)", + "quantity": null, + "unit": null, + "text_quantity": "- 红椒 少许 (切丝,装饰用)", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-贵州辣子鸡-贵州辣子鸡", + "name": "贵州辣子鸡的做法", + "description": "# 贵州辣子鸡的做法\n\n![贵州辣子鸡](./贵州辣子鸡.jpg)\n\n贵州人对吃鸡的执恋\n\n* 过节日,吃鸡\n* 过生日,吃鸡\n* 生病,吃鸡\n* 有客人,吃鸡\n* 家人团聚,吃鸡\n* 不知道吃什么,那就吃鸡\n\n贵州辣子鸡多种配菜,香辣可口,香糯软烂\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/贵州辣子鸡/贵州辣子鸡.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/贵州辣子鸡/贵州辣子鸡.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/贵州辣子鸡/贵州辣子鸡.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "农村玉米鸡", + "quantity": null, + "unit": null, + "text_quantity": "- 农村玉米鸡", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "花椒 or 麻椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 or 麻椒", + "notes": "量未指定" + }, + { + "name": "糍粑辣椒(花溪党武的辣椒,遵义的子弹头,条子椒,大方的皱椒混合之后打碎的辣椒)", + "quantity": null, + "unit": null, + "text_quantity": "- 糍粑辣椒(花溪党武的辣椒,遵义的子弹头,条子椒,大方的皱椒混合之后打碎的辣椒)", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "啤酒", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗", + "notes": "量未指定" + }, + { + "name": "酒糟", + "quantity": null, + "unit": null, + "text_quantity": "- 酒糟", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "鸡三到四个人的量是四斤,人多可以依次累加", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡三到四个人的量是四斤,人多可以依次累加", + "notes": "量未指定" + }, + { + "name": "啤酒半瓶", + "quantity": null, + "unit": null, + "text_quantity": "- 啤酒半瓶", + "notes": "量未指定" + }, + { + "name": "姜手指头大小两个", + "quantity": null, + "unit": null, + "text_quantity": "- 姜手指头大小两个", + "notes": "量未指定" + }, + { + "name": "糍粑辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 糍粑辣椒 500g,会是拳头大小两坨", + "notes": "量未指定" + }, + { + "name": "蒜苗三根", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗三根", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 2 片", + "notes": "量未指定" + }, + { + "name": "大蒜两个", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜两个", + "notes": "量未指定" + }, + { + "name": "土豆两个", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆两个", + "notes": "量未指定" + }, + { + "name": "菜籽油两斤,开始炸鸡会用很多", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油两斤,开始炸鸡会用很多", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 20 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "在锅中加入和锅一半高度的油,将切成长条的土豆先炸至表面金黄然后捞出备用,等油温上至烤手时候将切好的鸡块放入锅中炸,并放入切好的生姜片和花椒" + }, + { + "step": 2, + "description": "刚开始炸鸡的时候,油是浑浊的,因为鸡块里面有水的原因,等到油炸至清澈,鸡块就炸好了,然后捞出备用" + }, + { + "step": 3, + "description": "现在锅里面的油可以捞三分之一出来,现在用不到这么多的油" + }, + { + "step": 4, + "description": "将锅中剩余的油加热,加入糍粑辣椒,豆瓣酱,生姜片,炒出红油状,将炸好的鸡块翻炒均匀" + }, + { + "step": 5, + "description": "等到鸡块都上色,加入老抽,倒入啤酒,啤酒一定要盖过鸡块,加上香叶盖上盖,闷十分钟,期间间隔翻炒" + }, + { + "step": 6, + "description": "然后加入土豆条,大蒜(不用切,一颗一颗的最好),然后再闷 20 分钟" + }, + { + "step": 7, + "description": "最后加入酒糟翻炒均匀再加入切好的蒜苗,就可以出锅了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-酱排骨-酱排骨", + "name": "酱排骨的做法", + "description": "# 酱排骨的做法\n\n酱排骨其色泽酱红,肉质酥烂,骨香浓郁,汁浓味鲜,咸中带甜。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/酱排骨/酱排骨.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱排骨/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱排骨/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱排骨/2.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨 300 g", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 20 g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10 ml", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 30 ml", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 15 g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10 ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5 ml", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 1 个", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2 粒", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 2 片", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "起锅烧热水,放入排骨、姜片、料酒,煮开后用勺子舀去白色油沫,2-3 分钟后出锅" + }, + { + "step": 2, + "description": "冷水清洗排骨,清洗 2-3 遍" + }, + { + "step": 3, + "description": "小火起锅,加入食用油,加入白砂糖 ,轻轻搅拌到糖水变成黄色" + }, + { + "step": 4, + "description": "倒入排骨翻炒 30 S 后,加入生抽、蚝油、五香粉、蒜、小米椒后翻炒 30 S 后,加入清水没过排骨" + }, + { + "step": 5, + "description": "大火煮 30 分钟,加入老抽上色,再煮 10 分钟" + }, + { + "step": 6, + "description": "起锅摆盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-酱牛肉-酱牛肉", + "name": "酱牛肉的做法", + "description": "# 酱牛肉的做法\n\n![酱牛肉](./酱牛肉.jpg)\n\n家常酱牛肉营养丰富,味道香,不论是当作主食还是佐餐都很棒。一般初学者只需要 3 小时即可完成。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/meat_dish/酱牛肉/酱牛肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱牛肉/酱牛肉.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱牛肉/酱牛肉.jpg" + ], + "category": "荤菜", + "difficulty": 5, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "黄豆酱", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱", + "notes": "量未指定" + }, + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉 2000 克", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 1 片", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 3 片", + "notes": "量未指定" + }, + { + "name": "葱半根", + "quantity": null, + "unit": null, + "text_quantity": "- 葱半根", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 15ml", + "notes": "量未指定" + }, + { + "name": "桂皮", + "quantity": null, + "unit": null, + "text_quantity": "- 桂皮 1 块", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 7-8 粒", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 15 粒", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 30ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 8 克", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 4 个", + "notes": "量未指定" + }, + { + "name": "黄豆酱", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆酱 15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "牛肉浸泡 4-6 小时,加料酒、姜片,去除血水" + }, + { + "step": 2, + "description": "牛肉切成 8cm,不超过 10cm 的肉块" + }, + { + "step": 3, + "description": "牛肉放入锅中,加入冷水至水面没过牛肉,开锅至水沸腾开始计时,3 分钟后停火,捞出牛肉,用温水洗净" + }, + { + "step": 4, + "description": "将洗净后的牛肉放入砂锅或炖锅,加水没过牛肉,开大火,放入除盐之外的其他配料。" + }, + { + "step": 5, + "description": "水开之后,大火转为小火,持续加热 90 分钟,加盐" + }, + { + "step": 6, + "description": "加盐后,继续小火 90 分钟(注:每 30 分钟确认水位线,要求至少达到牛肉面高度的 80%)" + }, + { + "step": 7, + "description": "加热 180 分钟后,捞出牛肉,自然冷却,切片" + }, + { + "step": 8, + "description": "上桌食用,其他牛肉建议不切片冷藏。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-醉排骨-醉排骨", + "name": "醉排骨的做法", + "description": "# 醉排骨的做法\n\n醉排骨是福建省福州市特色传统名菜\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/醉排骨/醉排骨.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/醉排骨/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/醉排骨/1.jpeg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "鱼露", + "quantity": null, + "unit": null, + "text_quantity": "- 鱼露", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "地瓜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 地瓜粉", + "notes": "量未指定" + }, + { + "name": "鸡蛋黄", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋黄", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨 200 g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 10 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 500 ml", + "notes": "量未指定" + }, + { + "name": "鱼露", + "quantity": null, + "unit": null, + "text_quantity": "- 鱼露 5 ml", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻 5 g", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 5 g", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 5 ml", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 2 粒", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1 根", + "notes": "量未指定" + }, + { + "name": "地瓜粉", + "quantity": null, + "unit": null, + "text_quantity": "- 地瓜粉 30 g", + "notes": "量未指定" + }, + { + "name": "鸡蛋黄", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋黄 1 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "排骨中加入 5 g 地瓜粉和水进行搅拌,清洗 2-3 遍后放入大碗备用" + }, + { + "step": 2, + "description": "排骨中加入鱼露、地瓜粉、鸡蛋黄 充分搅拌" + }, + { + "step": 3, + "description": "将排骨一个一个放入锅中(切记不可以整碗倒入),保证每个不要粘在一起" + }, + { + "step": 4, + "description": "全部放入后,每 30 S 用勺子来回两面翻转瘦肉块,直至排骨表面金黄" + }, + { + "step": 5, + "description": "取出排骨,一分钟后倒入油锅中继续炸,直至瘦肉块表面出现焦黄后,取出放入大碗备用" + }, + { + "step": 6, + "description": "准备一个小碗,加入蒜末、香醋、白砂糖、鱼露、番茄酱、葱花、芝麻搅拌均匀,倒入 5 ml 热油" + }, + { + "step": 7, + "description": "将汤汁浇灌入排骨,在充分搅拌后倒入盘中" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-青椒土豆炒肉-青椒土豆炒肉", + "name": "青椒土豆炒肉的做法", + "description": "# 青椒土豆炒肉的做法\n\n![青椒土豆炒肉](https://user-images.githubusercontent.com/49046468/205808925-b0ab8f98-0325-4136-8094-3f2ae8c547d5.jpg)\n\n青椒土豆炒肉是一道荤素搭配的简单炒菜。一般初学者只需要 1 小时即可完成。贼下饭~\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/青椒土豆炒肉/青椒土豆炒肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/青椒土豆炒肉/青椒土豆炒肉.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/青椒土豆炒肉/青椒土豆炒肉.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "猪肉(五花肉)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉(五花肉)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "土豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆淀粉", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 2 个(共约 200g)", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 2 个(共约 300g)", + "notes": "量未指定" + }, + { + "name": "猪肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉 200g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1 根(约 10g)", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 1 块(约 5g)", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 3 瓣(约 12g)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 7g", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 6-10ml", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + }, + { + "name": "土豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆淀粉 5g", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 15g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "青椒去除根蒂切小块,土豆去皮切 2mm 薄片,猪肉切 4mm 薄片,葱横纵切 3mm 小段,姜蒜去皮拍散剁碎末;土豆淀粉加入约 15g 水搅拌均匀至水淀粉。" + }, + { + "step": 2, + "description": "起锅烧油,加热至 7 成热放入猪肉片,缓缓翻滚炒至去肉红色,加入约 3ml 酱油,翻炒肉片均匀上色,放入约 2g 盐。" + }, + { + "step": 3, + "description": "转 5 成油温,加入葱姜蒜炒 5 秒,然后加入土豆片,转 7 成油温均匀翻炒,加入加入约 5ml 酱油和 2g 盐,炒至土豆断生,表面轻微焦黄。" + }, + { + "step": 4, + "description": "转 8 成油温加入青椒,大火煸炒出锅气(有白烟冒出),反复均匀翻炒 1 分钟上色,最后在锅周围倒入水淀粉转 4 成火勾芡。" + }, + { + "step": 5, + "description": "在外观*呈粘稠状态*后关火,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-香干芹菜炒肉-香干芹菜炒肉", + "name": "香干芹菜炒肉的做法", + "description": "# 香干芹菜炒肉的做法\n\n![香干芹菜炒肉](./香干芹菜炒肉.jpg)\n\n香干芹菜炒肉是一道非常简单的家常菜小炒,据说多吃芹菜对于高血压有很好的缓解作用,加上香干和猪肉一起翻炒,还是很美味的。一般初学者只需要 30 分钟(含配菜时间)即可完成。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/香干芹菜炒肉/香干芹菜炒肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香干芹菜炒肉/香干芹菜炒肉.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香干芹菜炒肉/香干芹菜炒肉.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "豆干", + "quantity": null, + "unit": null, + "text_quantity": "- 豆干", + "notes": "量未指定" + }, + { + "name": "香芹/芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香芹/芹菜", + "notes": "量未指定" + }, + { + "name": "猪肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "辣椒:青椒或者红椒都可以", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒:青椒或者红椒都可以", + "notes": "量未指定" + }, + { + "name": "花椒:可选", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒:可选", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡精:可选", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精:可选", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "豆干:150g", + "quantity": null, + "unit": null, + "text_quantity": "- 豆干:150g", + "notes": "量未指定" + }, + { + "name": "香芹:4 根", + "quantity": null, + "unit": null, + "text_quantity": "- 香芹:4 根", + "notes": "量未指定" + }, + { + "name": "猪肉:200g", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉:200g", + "notes": "量未指定" + }, + { + "name": "蒜头:2 瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头:2 瓣", + "notes": "量未指定" + }, + { + "name": "辣椒:4 个", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒:4 个", + "notes": "量未指定" + }, + { + "name": "花椒:6 粒(不喜欢可以不放,或者放花椒水)", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒:6 粒(不喜欢可以不放,或者放花椒水)", + "notes": "量未指定" + }, + { + "name": "盐:5g", + "quantity": null, + "unit": null, + "text_quantity": "- 盐:5g", + "notes": "量未指定" + }, + { + "name": "鸡精:3g", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精:3g", + "notes": "量未指定" + }, + { + "name": "老抽:8ml", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽:8ml", + "notes": "量未指定" + }, + { + "name": "蚝油:5ml", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油:5ml", + "notes": "量未指定" + }, + { + "name": "食用油:10-15ml", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油:10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "芹菜去叶切段、切成不超过 4cm 的条状,备用" + }, + { + "step": 2, + "description": "香干切条,宽约小拇指,备用" + }, + { + "step": 3, + "description": "蒜头切片或者剁成蒜泥都行,备用" + }, + { + "step": 4, + "description": "辣椒切圈或者斜切成条都行,备用" + }, + { + "step": 5, + "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高" + }, + { + "step": 6, + "description": "放入花椒、大蒜爆香(可以吃姜的也可以额外放入一些姜片/姜丝)" + }, + { + "step": 7, + "description": "加入猪肉炒至变色,再加入 8ml 老抽上色翻炒均匀(有豆瓣酱的,可以放入 3ml 豆瓣酱一起翻炒)" + }, + { + "step": 8, + "description": "加入香干翻炒均匀(大约 2 分钟)" + }, + { + "step": 9, + "description": "加入辣椒翻炒均匀(大约 1-2 分钟)" + }, + { + "step": 10, + "description": "加入芹菜,放入 5g 盐翻炒 1 分钟" + }, + { + "step": 11, + "description": "加入 3g 鸡精、5ml 蚝油翻炒均匀,即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-香煎五花肉-香煎五花肉", + "name": "香煎五花肉的做法", + "description": "# 香煎五花肉的做法\n\n![香煎五花肉](./香煎五花肉.jpg)\n\n香煎五花肉一道简单易上手的菜。五花肉肥而不腻,生菜叶脆爽健康。稍微有下厨经验的人半小时便可制作完毕。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/香煎五花肉/香煎五花肉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香煎五花肉/香煎五花肉.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香煎五花肉/香煎五花肉.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "五花肉条(推荐长宽高为", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉条(推荐长宽高为 20cm\\*6cm\\*5cm)", + "notes": "量未指定" + }, + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜", + "notes": "量未指定" + }, + { + "name": "酱油,盐,味精,料酒,姜蒜,油,豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油,盐,味精,料酒,姜蒜,油,豆瓣酱", + "notes": "量未指定" + }, + { + "name": "五花肉条(推荐长宽高为", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉条(推荐长宽高为 20cm\\*6cm\\*5cm)", + "notes": "量未指定" + }, + { + "name": "生菜一朵", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜一朵", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将五花肉条沿长边切片,每片厚 1mm-1.5mm,备用" + }, + { + "step": 2, + "description": "将切好的五花肉放置碗中,依次加入 8g 酱油,1g 盐,1g 味精,10g 料酒,两片姜,两朵拍扁的大蒜腌制 10 分钟" + }, + { + "step": 3, + "description": "将生菜叶直接用手扒下来,洗干净,备用" + }, + { + "step": 4, + "description": "热锅,倒入 5ml 食用油。油轻微冒烟后下入五花肉。单面煎制焦黄色后翻面,另一边同理。" + }, + { + "step": 5, + "description": "五花肉出锅后,装盘。" + }, + { + "step": 6, + "description": "将豆瓣酱抹到菜叶上,卷着五花肉即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-香菇滑鸡-香菇滑鸡", + "name": "香菇滑鸡的做法", + "description": "# 香菇滑鸡的做法\n\n![香菇滑鸡](./香菇滑鸡.jpg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/香菇滑鸡/香菇滑鸡.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香菇滑鸡/香菇滑鸡.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香菇滑鸡/香菇滑鸡.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "大鸡腿", + "quantity": null, + "unit": null, + "text_quantity": "- 大鸡腿", + "notes": "量未指定" + }, + { + "name": "干香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 干香菇", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "大鸡腿", + "quantity": null, + "unit": null, + "text_quantity": "- 大鸡腿 2 个", + "notes": "量未指定" + }, + { + "name": "干香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 干香菇 5 粒", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 2 片", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 2 颗", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2 瓣", + "notes": "量未指定" + }, + { + "name": "温水(30-40 ℃)", + "quantity": null, + "unit": null, + "text_quantity": "- 温水(30-40 ℃) 150ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 15ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 30ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1.5g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 15ml", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 15ml", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "温水泡发干香菇" + }, + { + "step": 2, + "description": "姜切小块,葱切段,蒜对半切小粒" + }, + { + "step": 3, + "description": "鸡腿去骨(不去骨也可,只是略影响程序员吃饭的效率而已),切成小块" + }, + { + "step": 4, + "description": "泡发的香菇一分为四,香菇水留着备用" + }, + { + "step": 5, + "description": "鸡腿肉焯水 1 分钟,去除血沫和杂质" + }, + { + "step": 6, + "description": "鸡腿肉中加料酒 15ml、生抽 15ml、盐 1.5g、老抽 15ml,抓匀" + }, + { + "step": 7, + "description": "油温 3 成,下入鸡腿肉煸炒,等鸡腿肉金黄后盛出备用" + }, + { + "step": 8, + "description": "锅留底油,下入葱、姜、蒜炒香,香菇入锅,大火翻匀" + }, + { + "step": 9, + "description": "等待 20 秒会有香菇香味从锅中飘出,此时下入煸炒过的鸡腿肉,下入香菇水(全部,**本程序员认为的灵魂操作**)、糖 15ml、生抽 30ml" + }, + { + "step": 10, + "description": "转中火不盖盖,咕嘟 2 分钟收浓汤汁,淋入香油 5ml,撒上葱花后即可关火、装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-香辣鸡爪煲-香辣鸡爪煲", + "name": "香辣鸡爪煲的做法", + "description": "# 香辣鸡爪煲的做法\n\n![香辣鸡爪煲](./result2.jpg)\n\n香辣鸡爪煲口感 Q 弹,香辣浓郁,回味无穷。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/香辣鸡爪煲/香辣鸡爪煲.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香辣鸡爪煲/result1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香辣鸡爪煲/result1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香辣鸡爪煲/result2.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡爪", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡爪", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶", + "notes": "量未指定" + }, + { + "name": "八角", + "quantity": null, + "unit": null, + "text_quantity": "- 八角", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "辣椒面(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒面(可选)", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "一斤鸡爪", + "quantity": null, + "unit": null, + "text_quantity": "- 一斤鸡爪", + "notes": "量未指定" + }, + { + "name": "香叶", + "quantity": null, + "unit": null, + "text_quantity": "- 香叶 3 片", + "notes": "量未指定" + }, + { + "name": "八角三个", + "quantity": null, + "unit": null, + "text_quantity": "- 八角三个", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 6 个", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末 10g", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末 10g", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 2 根", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 3g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "给鸡爪剪去指甲。如果买的鸡爪只有脚掌部分,对半切开即可。 如果是整只鸡爪,需要去骨。清水洗干净。" + }, + { + "step": 2, + "description": "鸡爪冷水下锅,葱姜料酒焯水,水开,撇去浮沫。" + }, + { + "step": 3, + "description": "加入香叶、八角、生抽、老抽,盖盖小火慢煮三十分钟。" + }, + { + "step": 4, + "description": "捞出鸡爪,留一碗鸡汤备用。" + }, + { + "step": 5, + "description": "起锅烧油,用小火炒香姜末、蒜末、小米椒,能吃辣再放点辣椒面。加入生抽、老抽、蚝油、五香粉、盐,炒出酱香味。" + }, + { + "step": 6, + "description": "放入鸡爪,放一点盐调味,翻炒一两分钟,再倒入鸡汤,边炒边搅动。" + }, + { + "step": 7, + "description": "放入鸡精提鲜,撒入葱段搅拌均匀即可出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-鱼香茄子-鱼香茄子", + "name": "鱼香茄子的做法", + "description": "# 鱼香茄子的做法\n\n![yuxiangqiezi](./yxqz1.jpg)\n\n这个菜真的超级下饭,当个干饭王吧。\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/鱼香茄子/鱼香茄子.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz2.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz3.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz4.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz5.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz6.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz7.jpg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子", + "notes": "量未指定" + }, + { + "name": "肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 肉末", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "水淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 水淀粉", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子 2 根", + "notes": "量未指定" + }, + { + "name": "肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 肉末 20g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3-5g", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 5-10g", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 10ml", + "notes": "量未指定" + }, + { + "name": "水淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 水淀粉 100ml", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 20-30g", + "notes": "量未指定" + }, + { + "name": "小葱、姜、蒜、小米辣 (根据自己口味)", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱、姜、蒜、小米辣 (根据自己口味)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将茄子切成条。" + }, + { + "step": 2, + "description": "将肉切成肉沫,葱姜蒜切碎、小米椒切丁。" + }, + { + "step": 3, + "description": "调鱼香汁:碗中放入盐、味精、糖、生抽、老抽、醋、水淀粉搅拌均匀。" + }, + { + "step": 4, + "description": "锅中倒入 300ml 油,开小火(小火容易掌控),等油温七成热(小火大约 40 秒,有烟冒出)放入茄子炸两分钟,当茄子边缘微黄就捞出。多出的油可以盛出以后炒菜用。" + }, + { + "step": 5, + "description": "锅中留 15-30ml 油,倒入肉沫炒至颜色变白就盛出来。" + }, + { + "step": 6, + "description": "锅中倒入 15-30ml 油,放入豆瓣酱、葱白、姜、蒜炒香,然后倒入肉沫翻炒均匀。" + }, + { + "step": 7, + "description": "加入 80-150ml 清水(水面预计茄子八成高度为准),倒入茄子、倒入料汁,爆炒入味收汁。最后放入葱翻炒均匀,就可以起锅了。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。", + "做法参考:[鱼香茄子详细步骤](https://www.zhms.cn/recipe/kbbrl.html?source=2)" + ] + }, + { + "id": "dishes-meat_dish-麻婆豆腐-麻婆豆腐", + "name": "麻婆豆腐的做法", + "description": "# 麻婆豆腐的做法\n\n![成品](./1.jpeg)\n\n这是参考麻婆豆腐创作的一道菜。富含有铁、钙、磷、镁等人体必需的多种微量元素,最重要的是非常下饭哦~\n\n预估烹饪难度:★★★", + "source_path": "dishes/meat_dish/麻婆豆腐/麻婆豆腐.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/麻婆豆腐/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/麻婆豆腐/1.jpeg" + ], + "category": "荤菜", + "difficulty": 3, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "内脂豆腐(推荐清美)", + "quantity": null, + "unit": null, + "text_quantity": "- 内脂豆腐(推荐清美)", + "notes": "量未指定" + }, + { + "name": "水果刀", + "quantity": null, + "unit": null, + "text_quantity": "- 水果刀", + "notes": "量未指定" + }, + { + "name": "咸鸭蛋(推荐留夫鸭的,这个是灵魂)", + "quantity": null, + "unit": null, + "text_quantity": "- 咸鸭蛋(推荐留夫鸭的,这个是灵魂)", + "notes": "量未指定" + }, + { + "name": "五花肉(超市的肉糜也行)", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉(超市的肉糜也行)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "小米椒(不吃辣的可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒(不吃辣的可选)", + "notes": "量未指定" + }, + { + "name": "香辣酱(推荐广乐的)", + "quantity": null, + "unit": null, + "text_quantity": "- 香辣酱(推荐广乐的)", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐", + "notes": "量未指定" + }, + { + "name": "酱油(味极鲜酱油)", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油(味极鲜酱油)", + "notes": "量未指定" + }, + { + "name": "1 盒内脂豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 1 盒内脂豆腐", + "notes": "量未指定" + }, + { + "name": "1 枚咸鸭蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 1 枚咸鸭蛋", + "notes": "量未指定" + }, + { + "name": "20-30g 五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 20-30g 五花肉", + "notes": "量未指定" + }, + { + "name": "两瓣大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 两瓣大蒜", + "notes": "量未指定" + }, + { + "name": "2 片生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 2 片生姜", + "notes": "量未指定" + }, + { + "name": "5 根小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 5 根小米辣", + "notes": "量未指定" + }, + { + "name": "5g 蒜蓉辣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 5g 蒜蓉辣酱", + "notes": "量未指定" + }, + { + "name": "20 颗花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 20 颗花椒", + "notes": "量未指定" + }, + { + "name": "3g 食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 3g 食盐", + "notes": "量未指定" + }, + { + "name": "10g 酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 10g 酱油", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "大蒜和生姜切碎,备用" + }, + { + "step": 2, + "description": "小米辣切成辣椒圈,备用" + }, + { + "step": 3, + "description": "五花肉切成肉糜(本来就是买的肉糜的跳过)" + }, + { + "step": 4, + "description": "肉糜中加入一半的食盐和味极鲜酱油,搅拌均匀,备用" + }, + { + "step": 5, + "description": "鸭蛋用菜刀竖着对半切开(注意安全),去除蛋黄(一定要去除,不然会腥),剩下的蛋白捣碎成大约 2 mm * 2 mm 大小,不用太碎,备用" + }, + { + "step": 6, + "description": "打开豆腐包装,用水果刀将在盒子中的豆腐划成大约 2.5 cm * 3 cm 大小,备用" + }, + { + "step": 7, + "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高" + }, + { + "step": 8, + "description": "调成小火,放入大蒜、生姜、辣椒圈、花椒、咸鸭蛋、蒜蓉辣酱翻炒 20 秒,炒出香味" + }, + { + "step": 9, + "description": "调成中火,放入肉糜,翻炒大约 1 分钟,肉炒变色" + }, + { + "step": 10, + "description": "调成小火,放入豆腐,将剩下的食盐、味极鲜酱油酱油均匀的洒在豆腐上" + }, + { + "step": 11, + "description": "从锅边倒入开水(不然豆腐容易破),没过豆腐即可" + }, + { + "step": 12, + "description": "开大火,水沸腾后立马转入中火,等待大约 10 分钟" + }, + { + "step": 13, + "description": "等到水只剩 1/5 并且豆腐表面已经入色,关火,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-黑椒牛柳-黑椒牛柳", + "name": "黑椒牛柳的做法", + "description": "# 黑椒牛柳的做法\n\n![黑椒牛柳成品](./黑椒牛柳.jpg)\n\n黑椒牛柳是一道简单易做的菜。蔬菜与肉类均衡,富含蛋白质,口味适合大多数人。一般初学者只需要 1 小时以内即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/meat_dish/黑椒牛柳/黑椒牛柳.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黑椒牛柳/黑椒牛柳.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黑椒牛柳/黑椒牛柳.jpg" + ], + "category": "荤菜", + "difficulty": 4, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛肉(可以用牛里脊肉或者牛排肉)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉(可以用牛里脊肉或者牛排肉)", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "菜椒(红/黄椒)", + "quantity": null, + "unit": null, + "text_quantity": "- 菜椒(红/黄椒)", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "黑胡椒(粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒(粉)", + "notes": "量未指定" + }, + { + "name": "黑椒(腌料)", + "quantity": null, + "unit": null, + "text_quantity": "- 黑椒(腌料)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油", + "notes": "量未指定" + }, + { + "name": "牛肉量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉量 = 份数 * 100 克 (视就餐者胃容量和锅容量酌情增减)", + "notes": "量未指定" + }, + { + "name": "洋葱量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱量 = 份数 * 1/12 个(即 3 人时约切 1/4 )", + "notes": "量未指定" + }, + { + "name": "菜椒量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 菜椒量 = 份数 * 1/12 个(即 3 人时约切 1/4 )", + "notes": "量未指定" + }, + { + "name": "盐量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 份数 * 1 克", + "notes": "量未指定" + }, + { + "name": "淀粉 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 = 份数 * 3 克", + "notes": "量未指定" + }, + { + "name": "黑椒腌料 = 参照所购商品的说明按比例腌制", + "quantity": null, + "unit": null, + "text_quantity": "- 黑椒腌料 = 参照所购商品的说明按比例腌制", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉 = 份数 * 1 克(实际上是随便撒)", + "notes": "量未指定" + }, + { + "name": "花生油 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 花生油 = 份数 * 10ml (实际上油量是依据菜量变动的,如对牛肉的量有增减请按对应比例变动)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将牛肉切条,长度最好控制在 8 厘米以下,厚度约 5-10 毫米,宽度约 1 厘米(要求不严格)" + }, + { + "step": 2, + "description": "利用腌料腌制牛肉,混合均匀后静置,用量与时间请参照商品说明,可以延长不能缩短。" + }, + { + "step": 3, + "description": "如果使用液态腌料,可以在腌制结束前三分钟撒一层黑胡椒粉,然后再加入淀粉,再次混合均匀后静置 20 分钟。" + }, + { + "step": 4, + "description": "开火,热锅,加入花生油。" + }, + { + "step": 5, + "description": "当能看到锅里的油冒出一丝烟时,放入牛肉,翻炒。" + }, + { + "step": 6, + "description": "开中火偏大,翻炒 2 分钟至牛肉外表变色(即不出现明显血色,有血色部分说明翻炒不到位)(此处应小心油滴溅射)。" + }, + { + "step": 7, + "description": "放入洋葱和菜椒,翻炒 2 分钟。" + }, + { + "step": 8, + "description": "加入盐,再次撒一份黑胡椒粉,翻炒 30 秒,搅拌均匀。" + }, + { + "step": 9, + "description": "观察洋葱已经变软即可关火,出锅,盛盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-meat_dish-黔式腊肠娃娃菜-黔式腊肠娃娃菜", + "name": "黔式腊肠娃娃菜的做法", + "description": "# 黔式腊肠娃娃菜的做法\n\n![黔式腊肠娃娃菜](./黔式腊肠娃娃菜.jpg)\n\n黔式腊肠娃娃菜不需要掌握火候,也无需调料,非常适合懒癌的菜。制作时间 15 分钟,口味近似于川菜、湘菜,却是西南菜系中鲜见的不辣菜式,咸鲜可口、南北皆宜。\n\n预估烹饪难度:★", + "source_path": "dishes/meat_dish/黔式腊肠娃娃菜/黔式腊肠娃娃菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黔式腊肠娃娃菜/黔式腊肠娃娃菜.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黔式腊肠娃娃菜/黔式腊肠娃娃菜.jpg" + ], + "category": "荤菜", + "difficulty": 1, + "tags": [ + "荤菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "黔式腊肠", + "quantity": null, + "unit": null, + "text_quantity": "- 黔式腊肠", + "notes": "量未指定" + }, + { + "name": "娃娃菜", + "quantity": null, + "unit": null, + "text_quantity": "- 娃娃菜", + "notes": "量未指定" + }, + { + "name": "黔式腊肠", + "quantity": null, + "unit": null, + "text_quantity": "- 黔式腊肠 200g", + "notes": "量未指定" + }, + { + "name": "娃娃菜", + "quantity": null, + "unit": null, + "text_quantity": "- 娃娃菜 300g", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 750ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅内放入 750ml 水,开火加热至沸腾" + }, + { + "step": 2, + "description": "放入腊肠,计时 13 分钟" + }, + { + "step": 3, + "description": "放入娃娃菜,计时 2 分钟" + }, + { + "step": 4, + "description": "关火,夹出腊肠及娃娃菜" + }, + { + "step": 5, + "description": "娃娃菜切段、腊肠切片,装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-凉皮", + "name": "凉皮的做法", + "description": "# 凉皮的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/semi-finished/凉皮.md", + "image_path": null, + "images": [], + "category": "半成品加工", + "difficulty": 3, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "凉皮、面筋", + "quantity": null, + "unit": null, + "text_quantity": "- 凉皮、面筋", + "notes": "量未指定" + }, + { + "name": "盐、鸡精、蚝油、生抽、老抽、香油、香醋、芝麻酱(原味芝麻酱最佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 盐、鸡精、蚝油、生抽、老抽、香油、香醋、芝麻酱(原味芝麻酱最佳)", + "notes": "量未指定" + }, + { + "name": "黄瓜、大蒜、绿豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜、大蒜、绿豆芽", + "notes": "量未指定" + }, + { + "name": "盆、碗、盘子、蒜臼", + "quantity": null, + "unit": null, + "text_quantity": "- 盆、碗、盘子、蒜臼", + "notes": "量未指定" + }, + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜 100g/人、绿豆芽 50g/人。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅中加入 500ml 水。煮沸。" + }, + { + "step": 2, + "description": "将绿豆芽放入锅中,大火煮 60 秒。豆芽捞出,过凉水,放入盘中备用。" + }, + { + "step": 3, + "description": "黄瓜切丝放入盘中备用" + }, + { + "step": 4, + "description": "将 10g 蒜瓣剥皮、放入蒜臼中加入 1g 盐。锤成蒜泥,加入 10g 自来水。放置备用。" + }, + { + "step": 5, + "description": "注:超市购买来的凉皮表面一般会有食用油,可以使用自来水清洗。面筋同样。" + }, + { + "step": 6, + "description": "注:清洗面筋之后,请用手将面筋中的大量水分挤出(不需过于用力)。" + }, + { + "step": 7, + "description": "准备小碗,加入 3g 盐、2g 鸡精、5g 生抽、1g 老抽、1g 香油、2g 蚝油、香醋 5g、(盐、香醋均可根据个人口味酌量添加,以上数据只是大众口味)。" + }, + { + "step": 8, + "description": "以上调料加入 25-35g 温水(据个人咸淡程度),使用筷子将其拌匀、溶解。静置一旁冷却。" + }, + { + "step": 9, + "description": "注:以下计量均为一人份,如果有 n 人,请自觉将计量乘以 n" + }, + { + "step": 10, + "description": "拿出小碗,将准备好的芝麻酱放入其中。" + }, + { + "step": 11, + "description": "加入 4g 盐、3g 鸡精、5g 生抽、1g 老抽、3g 蚝油。" + }, + { + "step": 12, + "description": "使用筷子将其调料与芝麻酱拌匀。" + }, + { + "step": 13, + "description": "加入 10g 清水将其拌匀。" + }, + { + "step": 14, + "description": "上一步骤重复 2、3 次(次数根据个人对芝麻酱的浓稠程度而定)。" + }, + { + "step": 15, + "description": "拿出之前准备好的小盆,加入之前准备好的凉皮。" + }, + { + "step": 16, + "description": "倒入盐水,使用筷子将其拌匀。随之盛入小碗(盐水一并倒入碗中)。" + }, + { + "step": 17, + "description": "豆芽放置凉皮上、面筋随后放上。" + }, + { + "step": 18, + "description": "将调配好的芝麻酱从面筋上方倒下。" + }, + { + "step": 19, + "description": "撒上黄瓜丝。" + }, + { + "step": 20, + "description": "如有喜爱可以加入辣椒油。" + }, + { + "step": 21, + "description": "色香味俱全的家常凉皮出炉!" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-半成品意面", + "name": "半成品意面的做法", + "description": "# 半成品意面的做法\n\n意大利面🍝和中国面条口感上的区别主要是因为它是由小麦品种中最硬质的杜兰(durum)磨粉制成的。\n\n预估烹饪难度:★", + "source_path": "dishes/semi-finished/半成品意面.md", + "image_path": null, + "images": [], + "category": "半成品加工", + "difficulty": 1, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "1 袋 半成品意大利面(推荐品牌圃美多)", + "quantity": null, + "unit": null, + "text_quantity": "- 1 袋 半成品意大利面(推荐品牌圃美多)", + "notes": "量未指定" + }, + { + "name": "50 ml 清水", + "quantity": null, + "unit": null, + "text_quantity": "- 50 ml 清水", + "notes": "量未指定" + }, + { + "name": "平底锅 或 微波炉", + "quantity": null, + "unit": null, + "text_quantity": "- 平底锅 或 微波炉", + "notes": "量未指定" + }, + { + "name": "2 人", + "quantity": null, + "unit": null, + "text_quantity": "- 2 人 1 顿 520g(以半成品为准)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "热锅" + }, + { + "step": 2, + "description": "将 50 ml 清水倒入平底锅" + }, + { + "step": 3, + "description": "将面条放入,炒 1 分钟" + }, + { + "step": 4, + "description": "将酱料倒入,翻炒 1 分钟" + }, + { + "step": 5, + "description": "装盘即可" + }, + { + "step": 6, + "description": "将面条放入「可用于微波炉加热」的盘子中" + }, + { + "step": 7, + "description": "将附带的酱料倒在面条上" + }, + { + "step": 8, + "description": "倒入 50 ml 清水" + }, + { + "step": 9, + "description": "700W 加热 2 分钟" + }, + { + "step": 10, + "description": "取出拌匀即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-牛油火锅底料", + "name": "牛油火锅底料的做法", + "description": "# 牛油火锅底料的做法\n\n重庆火锅又称毛肚火锅或麻辣火锅,是中国传统饮食方式之一。\n\n其起源于明末清初的重庆嘉陵江畔,该菜式也是朝天门等码头船工纤夫的粗放餐饮方式。\n\n其主要原料是牛毛肚、猪黄喉、鸭肠、牛血旺等。\n\n一般初学者只需要 1 小时即可完成。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/semi-finished/牛油火锅底料.md", + "image_path": null, + "images": [], + "category": "半成品加工", + "difficulty": 5, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "每份原料可制作", + "quantity": null, + "unit": null, + "text_quantity": "- 每份原料可制作 7.5 kg 火锅底料/火锅老油", + "notes": "量未指定" + }, + { + "name": "牛油", + "quantity": null, + "unit": null, + "text_quantity": "- 牛油 4500 g", + "notes": "量未指定" + }, + { + "name": "(色拉油 或 菜籽油)", + "quantity": null, + "unit": null, + "text_quantity": "- (色拉油 或 菜籽油) 1000 ml", + "notes": "量未指定" + }, + { + "name": "纯猪油", + "quantity": null, + "unit": null, + "text_quantity": "- 纯猪油 500 g", + "notes": "量未指定" + }, + { + "name": "豆瓣(郫县)", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣(郫县) 1000 g", + "notes": "量未指定" + }, + { + "name": "糍粑辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 糍粑辣椒 3000 g", + "notes": "量未指定" + }, + { + "name": "老姜(切片)", + "quantity": null, + "unit": null, + "text_quantity": "- 老姜(切片) 250 g", + "notes": "量未指定" + }, + { + "name": "大葱(切段)", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱(切段) 100 g", + "notes": "量未指定" + }, + { + "name": "洋葱(切丝)", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱(切丝) 100 g", + "notes": "量未指定" + }, + { + "name": "大蒜(切片)", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜(切片) 200 g", + "notes": "量未指定" + }, + { + "name": "豆鼓(剁碎)(永川)", + "quantity": null, + "unit": null, + "text_quantity": "- 豆鼓(剁碎)(永川) 10 g", + "notes": "量未指定" + }, + { + "name": "豆母子", + "quantity": null, + "unit": null, + "text_quantity": "- 豆母子 140 g", + "notes": "量未指定" + }, + { + "name": "红花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红花椒 150 g", + "notes": "量未指定" + }, + { + "name": "老油 ? 颗粒香料", + "quantity": null, + "unit": null, + "text_quantity": "- 老油 ? 颗粒香料 100 g : 整形香料 150 g", + "notes": "量未指定" + }, + { + "name": "麦芽粉(肉香)", + "quantity": null, + "unit": null, + "text_quantity": "- 麦芽粉(肉香) 12.5 g", + "notes": "量未指定" + }, + { + "name": "白酒(52%VOL)", + "quantity": null, + "unit": null, + "text_quantity": "- 白酒(52%VOL) 150 ml", + "notes": "量未指定" + }, + { + "name": "老油 ?? 干辣椒面", + "quantity": null, + "unit": null, + "text_quantity": "- 老油 ?? 干辣椒面 15 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅置旺火(大火)放入牛油烧至 八成热(240±10°C) 时放入 `老姜、大葱、洋葱、大蒜 (各100g)`,炸干(吸尽异味(牛油腥味))后捞出扔掉。" + }, + { + "step": 2, + "description": "放入 `(色拉油 || 菜籽油)、纯猪油`,等待锅中油温下降到 五成热(150±10°C) 时放入 `糍粑辣椒` 持续翻炒 5-8 分钟。" + }, + { + "step": 3, + "description": "放入 `豆瓣` 炒散,转用 **中小火** 慢炒至料渣略发白翻砂(发出沙沙声)。" + }, + { + "step": 4, + "description": "油在外观呈现樱桃红时放入 `姜片(150g)、大蒜(100g)` 炒香,大约 15 秒。" + }, + { + "step": 5, + "description": "放入 `豆鼓、豆母子` 炒香,放入 `红花椒、小茴香` 炒香。" + }, + { + "step": 6, + "description": "(老油) 此刻放入 颗粒香料" + }, + { + "step": 7, + "description": "放入 `麦芽粉` 炒散,放入 `白酒` 炒散。" + }, + { + "step": 8, + "description": "起锅装入容器中,静置于温度低的环境(10-20) 5 天后再使用效果最佳。" + }, + { + "step": 9, + "description": "起锅装入容器中放入 `干辣椒面` 搅匀置放 24 小时,等待制作 **老油**。" + }, + { + "step": 10, + "description": "将底料倒入锅中,加入 3/5 的开水用大火烧开 (底料:2/5 开水:3/5)。" + }, + { + "step": 11, + "description": "烧开后表面会出现泡沫,将泡沫撇净。" + }, + { + "step": 12, + "description": "转用 **中小火** 慢熬出味(约 25-30 分钟),过滤去渣。" + }, + { + "step": 13, + "description": "等待容器中 **油水分离** 后,将表面的 **油** 撇净(将油打出来) 装入另外的容器。" + }, + { + "step": 14, + "description": "将上一步所 **撇** 出来的 **油** 重新倒入 **净锅** 中,直至 **炼干** 油中水分起锅装入容器即为 **火锅老油**。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-速冻水饺", + "name": "速冻水饺的做法", + "description": "# 速冻水饺的做法\n\n饺子是一种源自中国的一种以面皮包馅、形如半月或元宝形的食物。饺子是在农历新年和冬至等节日的重要食品。通常由碎肉和蔬菜馅料包裹成一片薄生面团后包好密封。而饺子的缺点在于难以制作。不妨选择购买速冻水饺来快速在家里吃上热气腾腾的饺子。\n\n预估烹饪难度:★", + "source_path": "dishes/semi-finished/速冻水饺.md", + "image_path": null, + "images": [], + "category": "半成品加工", + "difficulty": 1, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "未过期的一袋速冻水饺", + "quantity": null, + "unit": null, + "text_quantity": "- 未过期的一袋速冻水饺", + "notes": "量未指定" + }, + { + "name": "一般一个人可以食用", + "quantity": null, + "unit": null, + "text_quantity": "- 一般一个人可以食用 7~10 个水饺", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "中火,将水倒入锅中,静候水煮沸。" + }, + { + "step": 2, + "description": "将饺子倒入锅中。" + }, + { + "step": 3, + "description": "倒入锅前可以适当用水过一下。" + }, + { + "step": 4, + "description": "倒入饺子后,可以用炒菜勺子或铲子搅水,但要注意不要铲到饺子上,以避免粘锅上撕破皮或互相粘连造成粘连处夹生。" + }, + { + "step": 5, + "description": "频率不需要太高,平均每 `30` 秒摇 `3` 秒,饺子浮起后不需要再做此步。" + }, + { + "step": 6, + "description": "饺子浮起及水再次煮沸后,用炒菜勺子盛起一个饺子观察,如果面皮有夹生可用炒菜勺子舀入 80ml 凉水,将水降温,然后继续煮至沸腾,此间重复此观察、搅拌操作,最多加两次水就能全熟。" + }, + { + "step": 7, + "description": "所有饺子浮起后(下饺子后约 8 分钟)用铲子或漏勺把饺子铲入盘或碗中,装盘后即可食用。" + }, + { + "step": 8, + "description": "吃完饺子后,等锅内水温降低,将水倒掉并用洗洁精及时刷锅,不然过段时间锅内煮过的面粉会在锅壁形成黏糊糊的物质。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-速冻馄饨", + "name": "速冻馄饨的做法", + "description": "# 速冻馄饨的做法\n\n馄饨是一种起源于中国的一种民间传统面食,[饺子](./速冻水饺.md)由其分化而出,有皮薄馅嫩、汤清味鲜的特点。\n\n预估烹饪难度:★★", + "source_path": "dishes/semi-finished/速冻馄饨.md", + "image_path": null, + "images": [], + "category": "半成品加工", + "difficulty": 2, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "未过期的一袋速冻馄饨(自带调味料包更佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 未过期的一袋速冻馄饨(自带调味料包更佳)", + "notes": "量未指定" + }, + { + "name": "电饭煲(推荐品牌小米智能电饭煲)", + "quantity": null, + "unit": null, + "text_quantity": "- 电饭煲(推荐品牌小米智能电饭煲)", + "notes": "量未指定" + }, + { + "name": "盐(速冻馄饨无调味料包时)", + "quantity": null, + "unit": null, + "text_quantity": "- 盐(速冻馄饨无调味料包时)", + "notes": "量未指定" + }, + { + "name": "鸡精(速冻馄饨无调味料包时)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精(速冻馄饨无调味料包时)", + "notes": "量未指定" + }, + { + "name": "胡椒粉(速冻馄饨无调味料包时)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉(速冻馄饨无调味料包时)", + "notes": "量未指定" + }, + { + "name": "香油(速冻馄饨无调味料包时)", + "quantity": null, + "unit": null, + "text_quantity": "- 香油(速冻馄饨无调味料包时)", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 1 根(可选)", + "notes": "量未指定" + }, + { + "name": "一般一个人一顿可以食用", + "quantity": null, + "unit": null, + "text_quantity": "- 一般一个人一顿可以食用 12~20 个馄饨", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将水倒入电饭煲中,按炖或煮的模式运行 35 分钟,此时揭开电饭煲应看到水为沸腾状态。" + }, + { + "step": 2, + "description": "将速冻馄饨小心放入水中,注意不要烫伤。" + }, + { + "step": 3, + "description": "放入电饭煲前可以适当用水过一下。" + }, + { + "step": 4, + "description": "如果馄饨有调料包,此时可一并加入水中。" + }, + { + "step": 5, + "description": "盖上电饭煲,按同样炖或煮的模式运行 20 分钟。" + }, + { + "step": 6, + "description": "将所有馄饨连同能没过所有馄饨的水一同盛入碗中。" + }, + { + "step": 7, + "description": "如果此前没有加入调料包,此时可按自身口味轻重加入盐、鸡精、胡椒粉、香油调味。" + }, + { + "step": 8, + "description": "也可撒上 5~8 片香菜叶佐味(仅适用于对香菜味道不敏感的人)。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-懒人蛋挞-懒人蛋挞", + "name": "懒人蛋挞的做法", + "description": "# 懒人蛋挞的做法\n\n![蛋挞成品](./懒人蛋挞.png)\n\n蛋挞是一道常见的可口甜品,通常而言制作蛋挞是需要调和蛋挞液和制作蛋挞皮的,这个过程比较复杂和耗时,但是网购半成品恰恰解决解决以上的难题,初学者只需大约 40 分就可以完成。从今往后只要家里有烤箱,就可以化身烘焙达人,帮家人烤蛋挞!\n\n预估烹饪难度:★★★", + "source_path": "dishes/semi-finished/懒人蛋挞/懒人蛋挞.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/懒人蛋挞/懒人蛋挞.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/懒人蛋挞/懒人蛋挞.png" + ], + "category": "半成品加工", + "difficulty": 3, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "需要烤箱", + "quantity": null, + "unit": null, + "text_quantity": "- 需要烤箱 1 个(有上下火功能的最佳,也可以没有)", + "notes": "量未指定" + }, + { + "name": "隔热手套", + "quantity": null, + "unit": null, + "text_quantity": "- 隔热手套 1 双", + "notes": "量未指定" + }, + { + "name": "网购蛋挞液", + "quantity": null, + "unit": null, + "text_quantity": "- 网购蛋挞液 1 盒,蛋挞皮 1 盒(附近的大超市也可以,比如家乐福、沃尔玛等等)", + "notes": "量未指定" + }, + { + "name": "蛋挞皮", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋挞皮 1 个", + "notes": "量未指定" + }, + { + "name": "蛋挞液约", + "quantity": null, + "unit": null, + "text_quantity": "- 蛋挞液约 10ml,到达挞皮的 4/5 最佳", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "烤箱 200 度,预热 10 分钟" + }, + { + "step": 2, + "description": "在烤盘上放上蛋挞皮,蛋挞皮中倒入蛋挞液约 10ml,具体分量需要看蛋挞皮大小,通常倒入 4/5 即可" + }, + { + "step": 3, + "description": "将烤盘放入烤箱内,上下火 190 度,烤 10 - 20 分。如果想快速烤出蛋挞液上的焦褐斑点,需要上火更高一些,通常是 200 - 210 度" + }, + { + "step": 4, + "description": "蛋挞液烤出焦褐斑点,蛋挞皮完全蓬松冒油即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-炸薯条-炸薯条", + "name": "炸薯条的做法", + "description": "# 炸薯条的做法\n\n![炸薯条](./炸薯条.jpg)\n\n薯条🍟是一种土豆🥔\\马铃薯🥔\\洋芋🥔切成条状之后再油炸而成的快餐食物(在有的国家可能不算快餐),非常适合。相较于油炸,空气炸锅可能会更加易于避免崩溃和实现异步非阻塞。相较于自己动手切土豆再洗去淀粉并喷上油,使用半成品薯条可能会显著减少热量摄入前的热量消耗,四舍五入就是会显著减少热量摄入~~前的热量消耗~~。\n\n预估烹饪难度:★★", + "source_path": "dishes/semi-finished/炸薯条/炸薯条.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/炸薯条/炸薯条.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/炸薯条/炸薯条.jpg" + ], + "category": "半成品加工", + "difficulty": 2, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "1 袋半成品薯条(推荐品牌麦肯)", + "quantity": null, + "unit": null, + "text_quantity": "- 1 袋半成品薯条(推荐品牌麦肯)", + "notes": "量未指定" + }, + { + "name": "1 个空气炸锅(喜欢脆的切忌小牌子)", + "quantity": null, + "unit": null, + "text_quantity": "- 1 个空气炸锅(喜欢脆的切忌小牌子)", + "notes": "量未指定" + }, + { + "name": "作为主食,1 人", + "quantity": null, + "unit": null, + "text_quantity": "- 作为主食,1 人 1 顿 400g(以半成品为准)", + "notes": "量未指定" + }, + { + "name": "作为小食,1 人", + "quantity": null, + "unit": null, + "text_quantity": "- 作为小食,1 人 1 顿 1/4 主食质量+-50g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "开封大分量半成品薯条注意开口要小,可以有效减少长久储藏下薯条表面结霜。" + }, + { + "step": 2, + "description": "插电,200℃预热 5 分钟。" + }, + { + "step": 3, + "description": "预热的目的是为了确保放入食材的时候锅内温度已经处于烹饪所需温度。" + }, + { + "step": 4, + "description": "注意,预热完再拿出薯条,不应等薯条软化后再炸制。" + }, + { + "step": 5, + "description": "取出薯条放入空气炸锅,200℃20 分钟。" + }, + { + "step": 6, + "description": "取出薯条的时候注意半成品薯条已经有油,所以要异步去做客户端内刀斯林的话需要使用夹持工具。" + }, + { + "step": 7, + "description": "5~10 分钟时可以拿出锅体晃动使薯条受热均匀也防止粘连。" + }, + { + "step": 8, + "description": "10 分钟~15 分钟时,拿出锅体,往已经干了的薯条表面喷 1 层面积为薯条表面积 2/3 的油。" + }, + { + "step": 9, + "description": "喜欢脆薯条的,取出后拿着锅体跳舞让空气经过薯条表面后装盘;喜欢软薯条的直接装盘。配合蘸酱或浇上酱汁更佳。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-空气炸锅羊排-空气炸锅羊排", + "name": "空气炸锅羊排的做法", + "description": "# 空气炸锅羊排的做法\n\n![示例菜成品](./羊排.jpg)\n\n空气炸锅羊排超级懒人版,味道尚可,主要看羊排的品质。\n\n- 烹饪总时长:40 分钟(准备 5 分钟+腌制 20 分钟+下锅 15 分钟)\n- 实际操作时间:10 分钟\n\n预估烹饪难度:★★★", + "source_path": "dishes/semi-finished/空气炸锅羊排/空气炸锅羊排.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅羊排/羊排.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅羊排/羊排.jpg" + ], + "category": "半成品加工", + "difficulty": 3, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "必备:黑椒混合牛排调味料(懒)", + "quantity": null, + "unit": null, + "text_quantity": "- 必备:黑椒混合牛排调味料(懒)", + "notes": "量未指定" + }, + { + "name": "必备:蒜蓉酱(推荐川娃子的,同样是因为懒)", + "quantity": null, + "unit": null, + "text_quantity": "- 必备:蒜蓉酱(推荐川娃子的,同样是因为懒)", + "notes": "量未指定" + }, + { + "name": "必备:厨房纸", + "quantity": null, + "unit": null, + "text_quantity": "- 必备:厨房纸", + "notes": "量未指定" + }, + { + "name": "可选:黄油(JD 买小盒装的,一片一小盒)", + "quantity": null, + "unit": null, + "text_quantity": "- 可选:黄油(JD 买小盒装的,一片一小盒)", + "notes": "量未指定" + }, + { + "name": "可选:烧烤料", + "quantity": null, + "unit": null, + "text_quantity": "- 可选:烧烤料", + "notes": "量未指定" + }, + { + "name": "可选:罗勒碎", + "quantity": null, + "unit": null, + "text_quantity": "- 可选:罗勒碎", + "notes": "量未指定" + }, + { + "name": "可选:空气炸锅烤架(用烤架油比较少,底下更容易熟,洗起来麻烦。不用的话比较入味。看个人选择啦)", + "quantity": null, + "unit": null, + "text_quantity": "- 可选:空气炸锅烤架(用烤架油比较少,底下更容易熟,洗起来麻烦。不用的话比较入味。看个人选择啦)", + "notes": "量未指定" + }, + { + "name": "羊排", + "quantity": null, + "unit": null, + "text_quantity": "- 羊排 1 片约 160g", + "notes": "量未指定" + }, + { + "name": "黑椒混合牛排调味料", + "quantity": null, + "unit": null, + "text_quantity": "- 黑椒混合牛排调味料 5g", + "notes": "量未指定" + }, + { + "name": "蒜蓉酱", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜蓉酱 20g", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 1 小盒 10g 或 烧烤料 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "羊排放入碗中清水洗净血水" + }, + { + "step": 2, + "description": "羊排用厨房纸吸干水分,双面抹上黑椒混合调味料、蒜蓉酱,静置腌制 20 分钟" + }, + { + "step": 3, + "description": "锡纸碗放上烤架,羊排放在烤架上,撒上罗勒碎,黄油或烧烤料放在羊排上,空气炸锅 180° 10 分钟" + }, + { + "step": 4, + "description": "羊排翻面,撒上罗勒碎,黄油(从锡纸碗里舀上来)或烧烤料放在羊排上,空气炸锅 180° 5 分钟(可以视个人喜好加一点时间,这里写的是不会焦的时间)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-空气炸锅鸡翅中-空气炸锅鸡翅中", + "name": "空气炸锅鸡翅中的做法", + "description": "# 空气炸锅鸡翅中的做法\n\n![鸡翅中](./鸡翅中_0.jpg)\n![鸡翅中](./鸡翅中_1.jpg)\n\n空气炸锅做鸡翅中方便,这样自带油脂的食物味道很好,比 KFC 的好吃,吃完不**用洗碗洗锅**。\n\n- 烹饪时长:40 分钟(准备 3 分钟+解冻 20 分钟+下锅 17 分钟)\n- 实际操作时间:5 分钟\n\n预估烹饪难度:★★", + "source_path": "dishes/semi-finished/空气炸锅鸡翅中/空气炸锅鸡翅中.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅鸡翅中/鸡翅中_0.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅鸡翅中/鸡翅中_0.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅鸡翅中/鸡翅中_1.jpg" + ], + "category": "半成品加工", + "difficulty": 2, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "可选:罗勒碎(撒上去纯粹为了好看)", + "quantity": null, + "unit": null, + "text_quantity": "- 可选:罗勒碎(撒上去纯粹为了好看)", + "notes": "量未指定" + }, + { + "name": "可选:云南单山蘸水(代替烧烤料)", + "quantity": null, + "unit": null, + "text_quantity": "- 可选:云南单山蘸水(代替烧烤料)", + "notes": "量未指定" + }, + { + "name": "鸡翅中", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡翅中 6 个(泰森奥尔良鸡翅中,其他品牌例如圣农嘟嘟翅可能会大一些,请自行根据食量斟酌)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡翅从冰箱拿出来,鸡翼面朝下放入锡纸烤盘,撒上罗勒碎,盖上保鲜膜自然解冻 20 分钟" + }, + { + "step": 2, + "description": "撒上罗勒碎,空气炸锅 200°C,10 分钟" + }, + { + "step": 3, + "description": "翻面,撒上罗勒碎,空气炸锅 200°C,7 分钟" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-semi-finished-速冻汤圆-速冻汤圆", + "name": "速冻汤圆的做法", + "description": "# 速冻汤圆的做法\n\n![速冻汤圆](./速冻汤圆.jpg)\n\n速冻汤圆是一道简单易做的菜。一般初学者只需要 6 分钟即可完成。\n\n预估烹饪难度:★", + "source_path": "dishes/semi-finished/速冻汤圆/速冻汤圆.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/速冻汤圆/速冻汤圆.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/速冻汤圆/速冻汤圆.jpg" + ], + "category": "半成品加工", + "difficulty": 1, + "tags": [ + "半成品加工" + ], + "servings": 1, + "ingredients": [ + { + "name": "速冻汤圆", + "quantity": null, + "unit": null, + "text_quantity": "- 速冻汤圆", + "notes": "量未指定" + }, + { + "name": "微波炉", + "quantity": null, + "unit": null, + "text_quantity": "- 微波炉", + "notes": "量未指定" + }, + { + "name": "速冻汤圆:11 个。数量取决于碗的大小。保证放入的汤圆最高不超过碗高度 -", + "quantity": null, + "unit": null, + "text_quantity": "- 速冻汤圆:11 个。数量取决于碗的大小。保证放入的汤圆最高不超过碗高度 - 5mm。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "取出速冻汤圆,放入碗中。" + }, + { + "step": 2, + "description": "倒入开水,直至浸没汤圆。" + }, + { + "step": 3, + "description": "微波炉高火 4 分钟。" + }, + { + "step": 4, + "description": "假如汤圆均已吸水膨胀,则已熟。" + }, + { + "step": 5, + "description": "如果没熟,再加热 1 分钟。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-奶油蘑菇汤", + "name": "奶油蘑菇汤的做法", + "description": "# 奶油蘑菇汤的做法\n\n预估烹饪难度:★\n\n---", + "source_path": "dishes/soup/奶油蘑菇汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 1, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "白蘑菇", + "quantity": null, + "unit": null, + "text_quantity": "- 白蘑菇", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油", + "notes": "量未指定" + }, + { + "name": "黑胡椒碎", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒碎", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水", + "notes": "量未指定" + }, + { + "name": "--", + "quantity": null, + "unit": null, + "text_quantity": "- --", + "notes": "量未指定" + }, + { + "name": "白蘑菇", + "quantity": null, + "unit": null, + "text_quantity": "- 白蘑菇 200 克", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 50 克", + "notes": "量未指定" + }, + { + "name": "黄油", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油 15 克", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 10 克", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 200 毫升", + "notes": "量未指定" + }, + { + "name": "淡奶油", + "quantity": null, + "unit": null, + "text_quantity": "- 淡奶油 30 毫升", + "notes": "量未指定" + }, + { + "name": "清水", + "quantity": null, + "unit": null, + "text_quantity": "- 清水 100 毫升", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 克", + "notes": "量未指定" + }, + { + "name": "黑胡椒碎", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒碎 1 克", + "notes": "量未指定" + }, + { + "name": "--", + "quantity": null, + "unit": null, + "text_quantity": "- --", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "--" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-小米粥", + "name": "小米粥的做法", + "description": "# 小米粥的做法\n\n小米含有多种维生素、氨基酸、脂肪和碳水化合物,营养价值较高,每 100 克小米含蛋白质 9.7 克、脂肪 3.5 克,都不低于稻、麦。\n\n一般粮食中不含有的胡萝卜素,而小米每 100 克含量 0.12 毫克,维生素 B1 的含量位居所有粮食之首。\n\n小米含糖也很高,每 100 克含糖 72.8 克,产热量比大米高许多。另外,小米也富含维生素 B1,B2 等\n\n预估烹饪难度:★★", + "source_path": "dishes/soup/小米粥.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 2, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "小米", + "quantity": null, + "unit": null, + "text_quantity": "- 小米", + "notes": "量未指定" + }, + { + "name": "水(山泉水最佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 水(山泉水最佳)", + "notes": "量未指定" + }, + { + "name": "小米", + "quantity": null, + "unit": null, + "text_quantity": "- 小米 100 克", + "notes": "量未指定" + }, + { + "name": "水(山泉水最佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 水(山泉水最佳) 2000 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "小米 100 克,放入碗中,用水轻淘一遍(用手搅拌一下,将水倒掉,只是去掉外面的浮灰,不可搓洗!!!)" + }, + { + "step": 2, + "description": "水烧开,务必烧开!!!" + }, + { + "step": 3, + "description": "水烧开沸腾时,将小米倒入锅内。(很容易被忽视的一个很重要的环节)" + }, + { + "step": 4, + "description": "搅拌使得小米不会粘连锅底,继续用大火熬 6-10 分钟,注意用中间穿插搅拌几次。" + }, + { + "step": 5, + "description": "改中火、文火熬 15-20 分钟,锅盖要错开一条缝,千万不能让小米油溜掉哟,中间继续搅拌几次,不要糊锅底" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-生汆丸子汤", + "name": "生汆丸子汤的做法", + "description": "# 生汆丸子汤的做法\n\n生汆丸子汤,吃的就是一个鲜、嫩、弹。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/soup/生汆丸子汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 4, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "盐量 = 猪肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 猪肉斤数 * 6 克", + "notes": "量未指定" + }, + { + "name": "胡椒粉量 = 猪肉斤数", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉量 = 猪肉斤数 * 2 克", + "notes": "量未指定" + }, + { + "name": "土豆淀粉 = 多少人的用量", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆淀粉 = 多少人的用量 * 40 克,本教程以一人用量算", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "肉改刀切开,肥瘦三七分" + }, + { + "step": 2, + "description": "上刀剁一剁,用刀背砸一砸,把肉筋打开打松疏" + }, + { + "step": 3, + "description": "剁一剁,砸一砸,剁成肉末,要想好吃得自己剁,机器打的太黏糊了" + }, + { + "step": 4, + "description": "每斤肉,6 克盐,1 克胡椒粉" + }, + { + "step": 5, + "description": "上手抓匀" + }, + { + "step": 6, + "description": "葱姜花椒水分次加,边加边搅,用手揉匀,让肉吸饱水。每斤肉末 80 克葱姜花椒水" + }, + { + "step": 7, + "description": "放入鸡蛋清,继续顺着一个方向搅" + }, + { + "step": 8, + "description": "加入 40 克土豆淀粉,搅匀" + }, + { + "step": 9, + "description": "加入熟豆油,这是为了保持其嫩滑弹的状态" + }, + { + "step": 10, + "description": "起锅烧水,烧开,改小火,似开非开的样子" + }, + { + "step": 11, + "description": "上手,挤丸子," + }, + { + "step": 12, + "description": "全部漂起来,用小火煮 1 分钟" + }, + { + "step": 13, + "description": "粉丝放碗底" + }, + { + "step": 14, + "description": "加木耳,黄花,小香葱并用盐、胡椒粉、鸡粉打底调味" + }, + { + "step": 15, + "description": "连汤带丸子冲如碗中" + }, + { + "step": 16, + "description": "淋 3-5 滴香油" + }, + { + "step": 17, + "description": "加一小颗香菜" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-番茄牛肉蛋花汤", + "name": "番茄牛肉蛋花汤的做法", + "description": "# 番茄牛肉蛋花汤的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/soup/番茄牛肉蛋花汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 3, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉", + "notes": "量未指定" + }, + { + "name": "番茄", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "葱、姜、蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱、姜、蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "牛肉切成薄片" + }, + { + "step": 2, + "description": "番茄切成小块" + }, + { + "step": 3, + "description": "葱切成葱花" + }, + { + "step": 4, + "description": "姜切成姜片" + }, + { + "step": 5, + "description": "蒜剁成蒜泥" + }, + { + "step": 6, + "description": "牛肉放入碗中" + }, + { + "step": 7, + "description": "加盐、胡椒粉腌制 15-20 分钟" + }, + { + "step": 8, + "description": "加水煮开" + }, + { + "step": 9, + "description": "加入姜片和牛肉片,煮至牛肉变色" + }, + { + "step": 10, + "description": "加入番茄块,煮至番茄变软" + }, + { + "step": 11, + "description": "打散鸡蛋液,缓慢地倒入锅中,用筷子搅拌形成蛋花" + }, + { + "step": 12, + "description": "加入盐和胡椒粉调味" + }, + { + "step": 13, + "description": "最后加入葱花,即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-皮蛋瘦肉粥", + "name": "皮蛋瘦肉粥的做法", + "description": "# 皮蛋瘦肉粥的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/soup/皮蛋瘦肉粥.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 3, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水", + "notes": "量未指定" + }, + { + "name": "皮蛋(松花蛋)", + "quantity": null, + "unit": null, + "text_quantity": "- 皮蛋(松花蛋)", + "notes": "量未指定" + }, + { + "name": "瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 瘦肉", + "notes": "量未指定" + }, + { + "name": "大米", + "quantity": null, + "unit": null, + "text_quantity": "- 大米", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "电饭锅", + "quantity": null, + "unit": null, + "text_quantity": "- 电饭锅", + "notes": "量未指定" + }, + { + "name": "小碗若干", + "quantity": null, + "unit": null, + "text_quantity": "- 小碗若干", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 1 升", + "notes": "量未指定" + }, + { + "name": "皮蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 皮蛋 2 颗", + "notes": "量未指定" + }, + { + "name": "瘦肉", + "quantity": null, + "unit": null, + "text_quantity": "- 瘦肉 100g", + "notes": "量未指定" + }, + { + "name": "大米", + "quantity": null, + "unit": null, + "text_quantity": "- 大米 150ml", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 1 棵", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 1 棵", + "notes": "量未指定" + }, + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜 4 叶", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 1 拇指块", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 5 ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2g", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 1g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10ml", + "notes": "量未指定" + }, + { + "name": "小葱 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用", + "notes": "量未指定" + }, + { + "name": "香菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用", + "notes": "量未指定" + }, + { + "name": "生菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "大米 - 洗净 - 放入电饭锅内胆 - 加入 1 升 饮用水" + }, + { + "step": 2, + "description": "瘦肉 - 洗净 - 简易晾去水分 - 加入 10ml 食用油 - 揉搓均匀 - 放入电饭锅内胆" + }, + { + "step": 3, + "description": "皮蛋 - 去壳 - 洗净 - 对半切开 - 分离蛋白蛋黄 - 蛋白简单切碎块 - 蛋黄揉碎 - 放入电饭锅内胆" + }, + { + "step": 4, + "description": "生姜 - 洗净 - 削皮 - 去除枯黄枯黑无法食用部分 - 切丝 - 放入电饭锅内胆" + }, + { + "step": 5, + "description": "小葱 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用" + }, + { + "step": 6, + "description": "香菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用" + }, + { + "step": 7, + "description": "生菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用" + }, + { + "step": 8, + "description": "酱油 + 蚝油 + 盐 + 胡椒粉 - 搅拌均匀 - 放入小碗备用" + }, + { + "step": 9, + "description": "主料 - 使用电饭锅煮粥模式煮熟" + }, + { + "step": 10, + "description": "配料 - 待主料煮熟后,生菜单独过一次热水,并与其余配料一同开盖加入主料中搅拌均匀" + }, + { + "step": 11, + "description": "酱料 - 待主料煮熟后,与其余配料一同开盖加入主料中搅拌均匀" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-米粥", + "name": "米粥的做法", + "description": "# 米粥的做法\n\n大米粥是一道以大米和水作為主要原料經大火煮沸熬製而成的美食,老少皆宜,米粥具有補脾、和胃、清肺功效。\n\n预估烹饪难度:★★", + "source_path": "dishes/soup/米粥.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 2, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "米", + "quantity": null, + "unit": null, + "text_quantity": "- 米", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "植物油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油(可选)", + "notes": "量未指定" + }, + { + "name": "一般一个人可以食用", + "quantity": null, + "unit": null, + "text_quantity": "- 一般一个人可以食用 60ml-110ml 的米。", + "notes": "量未指定" + }, + { + "name": "水的体积是米饭的体积的", + "quantity": null, + "unit": null, + "text_quantity": "- 水的体积是米饭的体积的 9-12 倍。", + "notes": "量未指定" + }, + { + "name": "一碗容量是", + "quantity": null, + "unit": null, + "text_quantity": "- 一碗容量是 500ml。", + "notes": "量未指定" + }, + { + "name": "中断大火加热的最晚时间 T1:1.5 分钟/500ml", + "quantity": null, + "unit": null, + "text_quantity": "- 中断大火加热的最晚时间 T1:1.5 分钟/500ml * 水体积", + "notes": "量未指定" + }, + { + "name": "米粥能够食用的最早时间 Tr:10 分钟/500ml", + "quantity": null, + "unit": null, + "text_quantity": "- 米粥能够食用的最早时间 Tr:10 分钟/500ml * 水体积", + "notes": "量未指定" + }, + { + "name": "油的质量 Mo:生米体积 /", + "quantity": null, + "unit": null, + "text_quantity": "- 油的质量 Mo:生米体积 / 10", + "notes": "量未指定" + }, + { + "name": "冷藏时间 Tc = 生米体积 /10 ml/分钟。", + "quantity": null, + "unit": null, + "text_quantity": "- 冷藏时间 Tc = 生米体积 /10 ml/分钟。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "(可选)将 Mo ml 的油与洗净的米混合,*尽量确保完全混合,即每粒米上至少都沾上少量油*" + }, + { + "step": 2, + "description": "(可选)将 米-油混合物品冷藏保存,冷藏时间 Tc。" + }, + { + "step": 3, + "description": "将米和水加入锅中。" + }, + { + "step": 4, + "description": "开大火,加热到 T1。" + }, + { + "step": 5, + "description": "在 T1 之前将火关小。**如果忘记此步骤,水可能会漫出而熄灭火焰。非常危险!**" + }, + { + "step": 6, + "description": "加热到 Tr。在 Tr 时关闭火源。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-紫菜蛋花汤", + "name": "紫菜蛋花汤的做法", + "description": "# 紫菜蛋花汤的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/soup/紫菜蛋花汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 2, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "紫菜", + "quantity": null, + "unit": null, + "text_quantity": "- 紫菜", + "notes": "量未指定" + }, + { + "name": "葱花", + "quantity": null, + "unit": null, + "text_quantity": "- 葱花", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "虾仁(个人口味,可加可不加)", + "quantity": null, + "unit": null, + "text_quantity": "- 虾仁(个人口味,可加可不加)", + "notes": "量未指定" + }, + { + "name": "10g 的干紫菜(喜欢紫菜的可以多放些)", + "quantity": null, + "unit": null, + "text_quantity": "- 10g 的干紫菜(喜欢紫菜的可以多放些)", + "notes": "量未指定" + }, + { + "name": "两个鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 两个鸡蛋", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "干紫菜用清水泡 15 分钟,捞起沥干水份备用。" + }, + { + "step": 2, + "description": "热锅,倒入 1.5 升清水、5ml 油、2g 盐。待水开后放入紫菜。" + }, + { + "step": 3, + "description": "紫菜烧开后 3 分钟,将打好的蛋液徐徐倒入锅内,30 秒既可起锅。" + }, + { + "step": 4, + "description": "撒上葱花,转小火 20 秒。" + }, + { + "step": 5, + "description": "关火,出锅前放入几滴香油,也有的会放入一点虾皮,味道也不错。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-罗宋汤", + "name": "罗宋汤的做法", + "description": "# 罗宋汤的做法\n\n罗宋汤是一道源自俄罗斯甜菜汤的汤品,在传入上海后有了本土化的做法。其制作较为简单,初学者只需要 2-3 小时即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/soup/罗宋汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 4, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "蔬菜高汤(欧芹、胡萝卜、洋葱三件套)", + "quantity": null, + "unit": null, + "text_quantity": "- 蔬菜高汤(欧芹、胡萝卜、洋葱三件套)", + "notes": "量未指定" + }, + { + "name": "牛肉高汤(可用〇汤宝代替)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉高汤(可用〇汤宝代替)", + "notes": "量未指定" + }, + { + "name": "牛肉(可选牛腩肉或牛尾肉)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉(可选牛腩肉或牛尾肉)", + "notes": "量未指定" + }, + { + "name": "番茄(番茄膏、番茄罐头)", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄(番茄膏、番茄罐头)", + "notes": "量未指定" + }, + { + "name": "牛肉高汤", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉高汤 500 mL", + "notes": "量未指定" + }, + { + "name": "牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 牛肉 250 g (可选用牛腩肉或牛尾肉)", + "notes": "量未指定" + }, + { + "name": "番茄罐头", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄罐头 2 罐 (可用番茄替代、但风味欠佳)", + "notes": "量未指定" + }, + { + "name": "番茄膏", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄膏 5 g (增加番茄风味)", + "notes": "量未指定" + }, + { + "name": "马铃薯", + "quantity": null, + "unit": null, + "text_quantity": "- 马铃薯 400 g", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 100 g", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 100 g", + "notes": "量未指定" + }, + { + "name": "欧芹", + "quantity": null, + "unit": null, + "text_quantity": "- 欧芹 100 g", + "notes": "量未指定" + }, + { + "name": "包菜", + "quantity": null, + "unit": null, + "text_quantity": "- 包菜 200 g", + "notes": "量未指定" + }, + { + "name": "红肠", + "quantity": null, + "unit": null, + "text_quantity": "- 红肠 100 - 200 g", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油 5 mL (橄榄油用于蔬菜的烹制,可以用植物油代替)", + "notes": "量未指定" + }, + { + "name": "植物油", + "quantity": null, + "unit": null, + "text_quantity": "- 植物油 5 mL (植物油用于牛肉的烹制,不能用橄榄油代替)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 18 g", + "notes": "量未指定" + }, + { + "name": "黑胡椒", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒 3 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "洋葱、胡萝卜、欧芹切 1cm 见方小丁" + }, + { + "step": 2, + "description": "红肠、马铃薯切 2cm 块" + }, + { + "step": 3, + "description": "包菜去梗后,手撕至 2cm 片" + }, + { + "step": 4, + "description": "牛肉撒盐 3 g 、黑胡椒 3 g 腌制 5 分钟" + }, + { + "step": 5, + "description": "平底锅烧热,加入植物油" + }, + { + "step": 6, + "description": "煎制牛肉,直至表面**焦黄色**(可以带生,千万别糊了),取出备用。" + }, + { + "step": 7, + "description": "汤锅烧热,加入橄榄油、洋葱丁、胡萝卜丁、欧芹丁" + }, + { + "step": 8, + "description": "炒至**洋葱透明**,加入番茄膏、番茄罐头" + }, + { + "step": 9, + "description": "加入牛肉、马铃薯丁,翻炒均匀" + }, + { + "step": 10, + "description": "加水没过食材,中火烹制 1 小时" + }, + { + "step": 11, + "description": "开锅加入包菜丁、红肠丁,搅拌均匀" + }, + { + "step": 12, + "description": "中火烹制半小时" + }, + { + "step": 13, + "description": "开盖加入剩余 15 g 盐,混合均匀后盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-腊八粥", + "name": "腊八粥的做法", + "description": "# 腊八粥的做法\n\n> 无论盛在哪里的腊八粥,自然会熬煮过去。一年的酸甜苦辣涩。—— 迷迭香《腊八粥》\n\n腊八粥,又称七宝五味粥、佛粥、大家饭等,是一种由多样食材熬制而成的粥。主要富含碳水化合物、磷镁元素和各类维生素等,不仅可以补充日常的能量,其中的莲子还有养心安神的作用,适合工作压力大的人食用。除去食材准备时间,一般只需要 3 小时即可完成。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/soup/腊八粥.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 4, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水", + "notes": "量未指定" + }, + { + "name": "大米", + "quantity": null, + "unit": null, + "text_quantity": "- 大米", + "notes": "量未指定" + }, + { + "name": "糯米", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米", + "notes": "量未指定" + }, + { + "name": "花生", + "quantity": null, + "unit": null, + "text_quantity": "- 花生", + "notes": "量未指定" + }, + { + "name": "红豆", + "quantity": null, + "unit": null, + "text_quantity": "- 红豆", + "notes": "量未指定" + }, + { + "name": "红枣", + "quantity": null, + "unit": null, + "text_quantity": "- 红枣", + "notes": "量未指定" + }, + { + "name": "粥锅(普通锅容易糊底,有条件可选择高压锅)", + "quantity": null, + "unit": null, + "text_quantity": "- 粥锅(普通锅容易糊底,有条件可选择高压锅)", + "notes": "量未指定" + }, + { + "name": "中号玻璃碗(或其他中号不锈钢容器)", + "quantity": null, + "unit": null, + "text_quantity": "- 中号玻璃碗(或其他中号不锈钢容器)", + "notes": "量未指定" + }, + { + "name": "小碗若干", + "quantity": null, + "unit": null, + "text_quantity": "- 小碗若干", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 1 L", + "notes": "量未指定" + }, + { + "name": "大米", + "quantity": null, + "unit": null, + "text_quantity": "- 大米 50 g", + "notes": "量未指定" + }, + { + "name": "糯米", + "quantity": null, + "unit": null, + "text_quantity": "- 糯米 50 g", + "notes": "量未指定" + }, + { + "name": "薏米", + "quantity": null, + "unit": null, + "text_quantity": "- 薏米 50 g", + "notes": "量未指定" + }, + { + "name": "黑米", + "quantity": null, + "unit": null, + "text_quantity": "- 黑米 50 g", + "notes": "量未指定" + }, + { + "name": "小米", + "quantity": null, + "unit": null, + "text_quantity": "- 小米 50 g", + "notes": "量未指定" + }, + { + "name": "莲子", + "quantity": null, + "unit": null, + "text_quantity": "- 莲子 25 g", + "notes": "量未指定" + }, + { + "name": "绿豆", + "quantity": null, + "unit": null, + "text_quantity": "- 绿豆 25 g", + "notes": "量未指定" + }, + { + "name": "红豆", + "quantity": null, + "unit": null, + "text_quantity": "- 红豆 25 g", + "notes": "量未指定" + }, + { + "name": "花生", + "quantity": null, + "unit": null, + "text_quantity": "- 花生 25 g", + "notes": "量未指定" + }, + { + "name": "黄豆", + "quantity": null, + "unit": null, + "text_quantity": "- 黄豆 25 g", + "notes": "量未指定" + }, + { + "name": "豌豆", + "quantity": null, + "unit": null, + "text_quantity": "- 豌豆 25 g", + "notes": "量未指定" + }, + { + "name": "红枣", + "quantity": null, + "unit": null, + "text_quantity": "- 红枣 25 g", + "notes": "量未指定" + }, + { + "name": "桂圆", + "quantity": null, + "unit": null, + "text_quantity": "- 桂圆 25 g", + "notes": "量未指定" + }, + { + "name": "栗子", + "quantity": null, + "unit": null, + "text_quantity": "- 栗子 25 g", + "notes": "量未指定" + }, + { + "name": "去壳核桃", + "quantity": null, + "unit": null, + "text_quantity": "- 去壳核桃 25 g", + "notes": "量未指定" + }, + { + "name": "葡萄干", + "quantity": null, + "unit": null, + "text_quantity": "- 葡萄干 25 g", + "notes": "量未指定" + }, + { + "name": "红腰豆", + "quantity": null, + "unit": null, + "text_quantity": "- 红腰豆 25 g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 10~25 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "提前洗净好绿豆、红豆、花生、黄豆、豌豆、红腰豆,并用干净的玻璃碗盛放好,注入 3/4 玻璃碗大小的饮用水,浸泡一夜(或最少 8 小时)。" + }, + { + "step": 2, + "description": "提前洗净好大米、糯米、薏米、黑米、小米、莲子,并用干净的玻璃碗盛放好,注入 3/4 玻璃碗大小的饮用水,浸泡 3 小时。" + }, + { + "step": 3, + "description": "将步骤 1 中准备好的盛有绿豆、红豆、花生、黄豆、豌豆、红腰豆的玻璃碗中的水分分离倒出,其余原料倒入粥锅中,加入 1 升饮用水(或漫过食材 1 拇指块),大火煮沸,煮沸后合上锅盖,小火煮 30 分钟。" + }, + { + "step": 4, + "description": "将步骤 2 中准备好的盛有大米、糯米、薏米、黑米、小米、莲子的玻璃碗中的水分分离倒出,其余原料继续倒入粥锅中,合上锅盖,小火煮 60 分钟。" + }, + { + "step": 5, + "description": "洗净好红枣、桂圆、栗子、核桃、葡萄干(其中红枣切成小片)、冰糖,倒入锅中,合上锅盖,小火煮 60 分钟。" + }, + { + "step": 6, + "description": "确认煮出的粥粘稠后即可关火、盛盘、食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-西红柿鸡蛋汤", + "name": "西红柿鸡蛋汤的做法", + "description": "# 西红柿鸡蛋汤的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/soup/西红柿鸡蛋汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 2, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "味素", + "quantity": null, + "unit": null, + "text_quantity": "- 味素", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "葱、姜、蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱、姜、蒜", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 1 个", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1-2 个(依照自己的口味而定,喜欢吃鸡蛋就放 2 个,一般就放 1 个)", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 2 滴", + "notes": "量未指定" + }, + { + "name": "味素", + "quantity": null, + "unit": null, + "text_quantity": "- 味素 5 克(可选)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 15 克", + "notes": "量未指定" + }, + { + "name": "葱、姜、蒜共", + "quantity": null, + "unit": null, + "text_quantity": "- 葱、姜、蒜共 15 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-金针菇汤", + "name": "金针菇汤的做法", + "description": "# 金针菇汤的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/soup/金针菇汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 2, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "金针菇", + "quantity": null, + "unit": null, + "text_quantity": "- 金针菇", + "notes": "量未指定" + }, + { + "name": "鸡蛋(如需要)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋(如需要)", + "notes": "量未指定" + }, + { + "name": "金针菇", + "quantity": null, + "unit": null, + "text_quantity": "- 金针菇 400-500 克 (市场里面售卖的一袋即可)", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 15 克", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精 5 克", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-陈皮排骨汤", + "name": "陈皮排骨汤的做法", + "description": "# 陈皮排骨汤的做法\n\n新鲜的排骨除了拿来烧或者炖之外,还可以用来煲汤,搭配广东陈皮煲出来的汤非常养生,对脾胃、肺及咽喉都有一定的滋补功效,熬夜党必备。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/soup/陈皮排骨汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 4, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "陈皮", + "quantity": null, + "unit": null, + "text_quantity": "- 陈皮", + "notes": "量未指定" + }, + { + "name": "西洋参", + "quantity": null, + "unit": null, + "text_quantity": "- 西洋参", + "notes": "量未指定" + }, + { + "name": "石斛", + "quantity": null, + "unit": null, + "text_quantity": "- 石斛", + "notes": "量未指定" + }, + { + "name": "玉竹", + "quantity": null, + "unit": null, + "text_quantity": "- 玉竹", + "notes": "量未指定" + }, + { + "name": "麦冬", + "quantity": null, + "unit": null, + "text_quantity": "- 麦冬", + "notes": "量未指定" + }, + { + "name": "煲汤盅", + "quantity": null, + "unit": null, + "text_quantity": "- 煲汤盅", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "排骨,用猪骨也替代也可,4-5 块", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨,用猪骨也替代也可,4-5 块", + "notes": "量未指定" + }, + { + "name": "陈皮(一般选用", + "quantity": null, + "unit": null, + "text_quantity": "- 陈皮(一般选用 8-20 年制),一般 1 块陈皮是 3 瓣,取 1 瓣即可", + "notes": "量未指定" + }, + { + "name": "西洋参(又名花旗参),9 片", + "quantity": null, + "unit": null, + "text_quantity": "- 西洋参(又名花旗参),9 片", + "notes": "量未指定" + }, + { + "name": "石斛,", + "quantity": null, + "unit": null, + "text_quantity": "- 石斛, 6 颗", + "notes": "量未指定" + }, + { + "name": "玉竹,", + "quantity": null, + "unit": null, + "text_quantity": "- 玉竹, 5 片", + "notes": "量未指定" + }, + { + "name": "麦冬,", + "quantity": null, + "unit": null, + "text_quantity": "- 麦冬, 7 个", + "notes": "量未指定" + }, + { + "name": "煲汤盅,按", + "quantity": null, + "unit": null, + "text_quantity": "- 煲汤盅,按 1 人份", + "notes": "量未指定" + }, + { + "name": "食盐 ,5g", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 ,5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "排骨用热水过一边,去血水" + }, + { + "step": 2, + "description": "陈皮、麦冬、玉竹、石斛和西洋参,冲洗干净即可" + }, + { + "step": 3, + "description": "煲汤盅洗干净" + }, + { + "step": 4, + "description": "打开煲汤盅,先放入排骨在底部,然后依次放入陈皮、麦冬、玉竹、石斛和西洋参" + }, + { + "step": 5, + "description": "加入热水进煲汤盅,水不宜太满" + }, + { + "step": 6, + "description": "煲汤容器加入水,炖煮 1.5 小时即可" + }, + { + "step": 7, + "description": "加入食盐,趁热饮用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-勾芡香菇汤-勾芡香菇汤", + "name": "勾芡香菇汤的做法", + "description": "# 勾芡香菇汤的做法\n\n鲜香菇除了拿来和肉炒外,其实拿来做浓浓的勾芡汤也是非常可口的。\n\n预估烹饪难度:★★★", + "source_path": "dishes/soup/勾芡香菇汤/勾芡香菇汤.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/3.jpeg" + ], + "category": "汤", + "difficulty": 3, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 香菇", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉", + "notes": "量未指定" + }, + { + "name": "鲜香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜香菇 2 朵", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 0.5 根", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 3 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10 ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 3 g", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 350 ml", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉 10 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "香菇切片(每片厚度 0.5-1 cm,厚点相对薄点更有嚼劲),放入大碗中,倒入 2g 食用盐 浸泡 15 分钟" + }, + { + "step": 2, + "description": "生粉倒入小碗中,加入 50ml 水,搅拌生粉直至融化没有颗粒(即水淀粉)" + }, + { + "step": 3, + "description": "倒掉碗中的盐水,适当去掉香菇本身的水分(方便下一步煎炸)【可选】" + }, + { + "step": 4, + "description": "小火,倒入油,待油开始冒小泡(小火 30s ,看每个锅的功率),倒入香菇,每面煎 10s 【可选】" + }, + { + "step": 5, + "description": "倒入开水 300ml ,调中火再煮 3-5 分钟" + }, + { + "step": 6, + "description": "倒入水淀粉,适当搅拌锅中汤汁后,加入 3g 盐、3 g ,最后撒上葱花出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-排骨苦瓜汤-排骨苦瓜汤", + "name": "排骨苦瓜汤的做法", + "description": "# 排骨苦瓜汤的做法\n\n排骨苦瓜汤是一道味道鲜美且容易烹饪的汤。不过汤的烹饪时间都较长,一般来说最好提前 4 个小时开始进行准备。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/soup/排骨苦瓜汤/排骨苦瓜汤.md", + "image_path": null, + "images": [], + "category": "汤", + "difficulty": 4, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "电压力锅(可以极大简化烹饪过程和时间)", + "quantity": null, + "unit": null, + "text_quantity": "- 电压力锅(可以极大简化烹饪过程和时间)", + "notes": "量未指定" + }, + { + "name": "砂锅(相比于炒锅更适合炖汤)", + "quantity": null, + "unit": null, + "text_quantity": "- 砂锅(相比于炒锅更适合炖汤)", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "苦瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 苦瓜", + "notes": "量未指定" + }, + { + "name": "虾皮", + "quantity": null, + "unit": null, + "text_quantity": "- 虾皮", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨 250g 到 500g", + "notes": "量未指定" + }, + { + "name": "苦瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 苦瓜 100g 到 200g", + "notes": "量未指定" + }, + { + "name": "虾皮", + "quantity": null, + "unit": null, + "text_quantity": "- 虾皮 5g 到 15g", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜 5~10g(可选,用于焯水时去腥)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "排骨洗净,切到约 4cm ±2cm * 3 ± 2cm 的小块(如没有剁排骨的工具,可以求助摊主)" + }, + { + "step": 2, + "description": "炒锅倒入冷水 700ml 和排骨一起加热至煮沸,关火捞出排骨" + }, + { + "step": 3, + "description": "苦瓜中间切为两半,清除干净内部的种子和苦瓜瓤,切为 0.5 ± 0.3 cm 的苦瓜条,洗净" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-昂刺鱼豆腐汤-昂刺鱼豆腐汤", + "name": "昂刺鱼豆腐汤的做法", + "description": "# 昂刺鱼豆腐汤的做法\n\n- 昂刺鱼/沙光鱼 豆腐汤 刺少 肉嫩 营养丰盛、适合任何年龄的小伙伴\n\n预估烹饪难度:★★★", + "source_path": "dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤01.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤01.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤02.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/沙光鱼豆腐汤.jpg" + ], + "category": "汤", + "difficulty": 3, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "昂刺鱼或者沙光鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 昂刺鱼或者沙光鱼", + "notes": "量未指定" + }, + { + "name": "豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 豆腐", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "昂刺鱼或者沙光鱼 一条", + "quantity": null, + "unit": null, + "text_quantity": "- 昂刺鱼或者沙光鱼 一条", + "notes": "量未指定" + }, + { + "name": "豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 豆腐 100 g", + "notes": "量未指定" + }, + { + "name": "香葱 一根", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 一根", + "notes": "量未指定" + }, + { + "name": "姜 一块", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 一块", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 3-5 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15 ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 10-15 g", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 1L", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鱼处理好后洗净,(特别注意肚内的血丝、不洗干净会有腥味),放入大碗中,倒入料酒、10g 姜片、5g 盐,腌制 15 分钟" + }, + { + "step": 2, + "description": "豆腐切块,放入凉水浸泡 5 分钟,捞出备用" + }, + { + "step": 3, + "description": "煎鱼前,先用生姜片擦一下锅防止粘锅,倒入油(油量为 15ml * 鱼的条数 ),烧热后放入鱼煎 2~3 分钟,期间需要晃动一下鱼,防止粘底,且需要翻一次身" + }, + { + "step": 4, + "description": "待鱼全部煎好之后,倒入开水、5ml 料酒、姜片,小火转至大火,盖上锅盖、大火煮 10 分钟(水要稍微多一些,后面会蒸发掉一些)" + }, + { + "step": 5, + "description": "见汤变白后倒入准备好的豆腐,调中火再煮 5 分钟,加入 10g 盐、3g 胡椒粉调味,最后撒上葱花出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-朱雀汤-朱雀汤", + "name": "朱雀汤的做法", + "description": "# 朱雀汤的做法\n\n预估烹饪难度:★", + "source_path": "dishes/soup/朱雀汤/朱雀汤.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/朱雀汤/朱雀汤.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/朱雀汤/朱雀汤.jpg" + ], + "category": "汤", + "difficulty": 1, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "香油(芝麻油)", + "quantity": null, + "unit": null, + "text_quantity": "- 香油(芝麻油)", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "一个鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 一个鸡蛋", + "notes": "量未指定" + }, + { + "name": "500ml 水", + "quantity": null, + "unit": null, + "text_quantity": "- 500ml 水", + "notes": "量未指定" + }, + { + "name": "20 克白糖(根据个人口味调整)", + "quantity": null, + "unit": null, + "text_quantity": "- 20 克白糖(根据个人口味调整)", + "notes": "量未指定" + }, + { + "name": "2ml 香油", + "quantity": null, + "unit": null, + "text_quantity": "- 2ml 香油", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋在碗中打散,再倒入香油。" + }, + { + "step": 2, + "description": "水烧开后,在沸腾状态下快速倒入盛有鸡蛋的碗中。" + }, + { + "step": 3, + "description": "放入白糖。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-玉米排骨汤-玉米排骨汤", + "name": "玉米排骨汤的做法", + "description": "# 玉米排骨汤的做法\n\n新鲜的排骨除了拿来烧或者炖之外,还可以用来煲汤,搭配玉米和胡萝卜煲出来的汤非常鲜美。\n\n预估烹饪难度:★★★", + "source_path": "dishes/soup/玉米排骨汤/玉米排骨汤.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/玉米排骨汤/玉米排骨汤.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/玉米排骨汤/玉米排骨汤.jpeg" + ], + "category": "汤", + "difficulty": 3, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "玉米", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米", + "notes": "量未指定" + }, + { + "name": "胡箩卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡箩卜", + "notes": "量未指定" + }, + { + "name": "生姜", + "quantity": null, + "unit": null, + "text_quantity": "- 生姜", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "砂锅(没有的话,用铁锅也行)", + "quantity": null, + "unit": null, + "text_quantity": "- 砂锅(没有的话,用铁锅也行)", + "notes": "量未指定" + }, + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨 500-800g", + "notes": "量未指定" + }, + { + "name": "玉米一根(喜欢吃玉米的可以多一根)", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米一根(喜欢吃玉米的可以多一根)", + "notes": "量未指定" + }, + { + "name": "胡萝卜一根(喜欢吃胡箩卜的可以多一根)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜一根(喜欢吃胡箩卜的可以多一根)", + "notes": "量未指定" + }, + { + "name": "大葱小半根", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱小半根", + "notes": "量未指定" + }, + { + "name": "香葱一根", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱一根", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10 ml", + "notes": "量未指定" + }, + { + "name": "黑胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 黑胡椒粉 4g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 10ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 10ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 10-15g(根据最后汤剩余的量而定)", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 1000 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "大葱切成 3-4cm 的大段,用刀背拍一下" + }, + { + "step": 2, + "description": "玉米剁成小块" + }, + { + "step": 3, + "description": "胡箩卜切成滚刀块" + }, + { + "step": 4, + "description": "生姜去皮切大片" + }, + { + "step": 5, + "description": "新鲜的排骨砍成小块" + }, + { + "step": 6, + "description": "排骨凉水下锅,放入大葱、生姜、料酒开始焯水,大火烧开,撇去浮沫,捞出排骨,沥干水分" + }, + { + "step": 7, + "description": "热锅凉油,切大片的生姜和排骨一起下锅煸炒,待排骨表面微微焦黄,放入醋(可加速肉质软烂),继续煸炒一分钟" + }, + { + "step": 8, + "description": "冲入开水,一次给足,之后就不要再加了,大火烧开" + }, + { + "step": 9, + "description": "先下入玉米,放入胡椒粉,盖盖小火炖二十分钟,然后放入胡萝卜,盖盖继续小火炖四十分钟" + }, + { + "step": 10, + "description": "调味很简单,出锅前三分钟,除了盐什么都不用放,最后撒上一把小葱花即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-羊肉汤-羊肉汤", + "name": "羊肉汤的做法", + "description": "# 羊肉汤的做法\n\n![羊杂汤](./羊肉汤.jpg)\n\n羊肉汤/羊肉汤简单易,有抵御寒冷、温润养胃、开胃健脾的功效,富含钙、铁、蛋白质等营养物质。\n\n预估烹饪难度:★★★", + "source_path": "dishes/soup/羊肉汤/羊肉汤.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/羊肉汤/羊肉汤.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/羊肉汤/羊肉汤.jpg" + ], + "category": "汤", + "difficulty": 3, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "羊肉或羊杂", + "quantity": null, + "unit": null, + "text_quantity": "- 羊肉或羊杂", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "孜然粉(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然粉(可选)", + "notes": "量未指定" + }, + { + "name": "香菜(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜(可选)", + "notes": "量未指定" + }, + { + "name": "羊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 羊肉 300g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 20ml", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 50g", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 1000ml", + "notes": "量未指定" + }, + { + "name": "白胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 白胡椒粉 1g", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 5g", + "notes": "量未指定" + }, + { + "name": "孜然粉", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然粉 1g", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "羊肉切成长 5cm 宽 0.5cm 的块" + }, + { + "step": 2, + "description": "大葱切成小段" + }, + { + "step": 3, + "description": "羊肉放入锅中,加入 1000ml 常温水,加入料酒、大葱" + }, + { + "step": 4, + "description": "煮沸 2 分钟后,捞出羊肉,使用常温水洗净,沥干水分" + }, + { + "step": 5, + "description": "热锅加入食用油,加入羊肉,翻炒 2 分钟至羊肉表面微黄" + }, + { + "step": 6, + "description": "加入开水,开到大火档位" + }, + { + "step": 7, + "description": "5 分钟后,加入白胡椒粉、盐,继续煮沸 5 分钟" + }, + { + "step": 8, + "description": "出锅之后,加入香菜、孜然粉,搅拌均匀" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-菌菇炖乳鸽-菌菇炖乳鸽", + "name": "菌菇炖乳鸽的做法", + "description": "# 菌菇炖乳鸽的做法\n\n- 菌菇炖乳鸽 汤鲜、肉嫩、营养丰富\n\n预估烹饪难度:★★★★", + "source_path": "dishes/soup/菌菇炖乳鸽/菌菇炖乳鸽.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/菌菇炖乳鸽/菌菇炖乳鸽.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/菌菇炖乳鸽/菌菇炖乳鸽.jpg" + ], + "category": "汤", + "difficulty": 4, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "乳鸽", + "quantity": null, + "unit": null, + "text_quantity": "- 乳鸽", + "notes": "量未指定" + }, + { + "name": "菌菇", + "quantity": null, + "unit": null, + "text_quantity": "- 菌菇", + "notes": "量未指定" + }, + { + "name": "玉米", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "瓦罐或者高压锅", + "quantity": null, + "unit": null, + "text_quantity": "- 瓦罐或者高压锅", + "notes": "量未指定" + }, + { + "name": "乳鸽", + "quantity": null, + "unit": null, + "text_quantity": "- 乳鸽 300 g", + "notes": "量未指定" + }, + { + "name": "菌菇", + "quantity": null, + "unit": null, + "text_quantity": "- 菌菇 100 g", + "notes": "量未指定" + }, + { + "name": "玉米", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米 200 g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 30 g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 15 ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 10 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "冷水洗干净热心摊主处理好的乳鸽" + }, + { + "step": 2, + "description": "冷水锅中放入洗干净的乳鸽,加入 15ml 料酒与姜,水煮开即可捞出乳鸽,要不然会丢失营养" + }, + { + "step": 3, + "description": "把乳鸽放到高压缩或者瓦罐中、倒入的水要没过乳鸽,放入生姜 20 g,玉米 200 g、菌菇 100 g" + }, + { + "step": 4, + "description": "时间到了,盛到碗中,加入 3~5g 盐 即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-银耳莲子粥-银耳莲子粥", + "name": "银耳莲子粥的做法", + "description": "# 银耳莲子粥的做法\n\n![银耳莲子粥](./银耳莲子粥.png)\n\n银耳莲子粥是一道营养非常丰富的粥。口味偏甜,具有养心安神的功效。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/soup/银耳莲子粥/银耳莲子粥.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/银耳莲子粥/银耳莲子粥.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/银耳莲子粥/银耳莲子粥.png" + ], + "category": "汤", + "difficulty": 4, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "银耳", + "quantity": null, + "unit": null, + "text_quantity": "- 银耳", + "notes": "量未指定" + }, + { + "name": "去心莲子", + "quantity": null, + "unit": null, + "text_quantity": "- 去心莲子", + "notes": "量未指定" + }, + { + "name": "红枣", + "quantity": null, + "unit": null, + "text_quantity": "- 红枣", + "notes": "量未指定" + }, + { + "name": "枸杞(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 枸杞(可选)", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖", + "notes": "量未指定" + }, + { + "name": "银耳", + "quantity": null, + "unit": null, + "text_quantity": "- 银耳 60g", + "notes": "量未指定" + }, + { + "name": "去心莲子", + "quantity": null, + "unit": null, + "text_quantity": "- 去心莲子 20g", + "notes": "量未指定" + }, + { + "name": "红枣", + "quantity": null, + "unit": null, + "text_quantity": "- 红枣 6g", + "notes": "量未指定" + }, + { + "name": "枸杞", + "quantity": null, + "unit": null, + "text_quantity": "- 枸杞 5-6g", + "notes": "量未指定" + }, + { + "name": "冰糖", + "quantity": null, + "unit": null, + "text_quantity": "- 冰糖 10-20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "把银耳、莲子用清水浸泡 2 个小时,红枣浸泡 10 - 20 分钟,枸杞洗净,备用" + }, + { + "step": 2, + "description": "在锅中倒入 600ml 水,烧开后依次放入银耳、莲子、红枣" + }, + { + "step": 3, + "description": "等待水再次烧开后,盖上锅盖,转至中火继续熬" + }, + { + "step": 4, + "description": "熬到大约 1 小时后,放入 5g - 10g 冰糖和 5g - 6g 枸杞,转至小火熬" + }, + { + "step": 5, + "description": "小火继续熬 30 分钟,此时银耳开始呈现粘稠状态" + }, + { + "step": 6, + "description": "再次放入 5g - 10g 冰糖,用勺子搅拌 5 - 10 分钟" + }, + { + "step": 7, + "description": "关火,用勺子盛出" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-soup-陈皮排骨汤-陈皮排骨汤", + "name": "陈皮排骨汤的做法", + "description": "# 陈皮排骨汤的做法\n\n新鲜的排骨除了拿来烧或者炖之外,还可以用来煲汤,搭配广东陈皮煲出来的汤非常养生,对脾胃、肺及咽喉都有一定的滋补功效,熬夜党必备。\n\n![陈皮排骨汤](./陈皮排骨汤.jpg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/soup/陈皮排骨汤/陈皮排骨汤.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/陈皮排骨汤/陈皮排骨汤.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/陈皮排骨汤/陈皮排骨汤.jpg" + ], + "category": "汤", + "difficulty": 3, + "tags": [ + "汤" + ], + "servings": 1, + "ingredients": [ + { + "name": "排骨", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨", + "notes": "量未指定" + }, + { + "name": "陈皮", + "quantity": null, + "unit": null, + "text_quantity": "- 陈皮", + "notes": "量未指定" + }, + { + "name": "西洋参", + "quantity": null, + "unit": null, + "text_quantity": "- 西洋参", + "notes": "量未指定" + }, + { + "name": "石斛", + "quantity": null, + "unit": null, + "text_quantity": "- 石斛", + "notes": "量未指定" + }, + { + "name": "玉竹", + "quantity": null, + "unit": null, + "text_quantity": "- 玉竹", + "notes": "量未指定" + }, + { + "name": "麦冬", + "quantity": null, + "unit": null, + "text_quantity": "- 麦冬", + "notes": "量未指定" + }, + { + "name": "煲汤盅", + "quantity": null, + "unit": null, + "text_quantity": "- 煲汤盅", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "排骨,用猪骨也替代也可,4-5 块", + "quantity": null, + "unit": null, + "text_quantity": "- 排骨,用猪骨也替代也可,4-5 块", + "notes": "量未指定" + }, + { + "name": "陈皮(一般选用", + "quantity": null, + "unit": null, + "text_quantity": "- 陈皮(一般选用 8-20 年制),一般 1 块陈皮是 3 瓣,取 1 瓣即可", + "notes": "量未指定" + }, + { + "name": "西洋参(又名花旗参),9 片", + "quantity": null, + "unit": null, + "text_quantity": "- 西洋参(又名花旗参),9 片", + "notes": "量未指定" + }, + { + "name": "石斛,", + "quantity": null, + "unit": null, + "text_quantity": "- 石斛, 6 颗", + "notes": "量未指定" + }, + { + "name": "玉竹,", + "quantity": null, + "unit": null, + "text_quantity": "- 玉竹, 5 片", + "notes": "量未指定" + }, + { + "name": "麦冬,", + "quantity": null, + "unit": null, + "text_quantity": "- 麦冬, 7 个", + "notes": "量未指定" + }, + { + "name": "煲汤盅,按", + "quantity": null, + "unit": null, + "text_quantity": "- 煲汤盅,按 1 人份", + "notes": "量未指定" + }, + { + "name": "食盐 ,5g", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 ,5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "排骨用热水过一边,去血水" + }, + { + "step": 2, + "description": "陈皮、麦冬、玉竹、石斛和西洋参,冲洗干净即可" + }, + { + "step": 3, + "description": "煲汤盅洗干净" + }, + { + "step": 4, + "description": "打开煲汤盅,先放入排骨在底部,然后依次放入陈皮、麦冬、玉竹、石斛和西洋参" + }, + { + "step": 5, + "description": "加入热水进煲汤盅,水不宜太满" + }, + { + "step": 6, + "description": "煲汤容器加入水,炖煮 1.5 小时即可" + }, + { + "step": 7, + "description": "加入食盐,趁热饮用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-手工水饺", + "name": "手工水饺的做法", + "description": "# 手工水饺的做法\n\n饺子是一道非常好吃的主食之一。饱肚且易于根据自己口味进行调味,适合在 US 的同学吃不到水饺解馋。一般初学者需要 3 小时完成,难度较大\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/staple/手工水饺.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 5, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "擀面杖", + "quantity": null, + "unit": null, + "text_quantity": "- 擀面杖", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "冷水", + "quantity": null, + "unit": null, + "text_quantity": "- 冷水", + "notes": "量未指定" + }, + { + "name": "直径", + "quantity": null, + "unit": null, + "text_quantity": "- 直径 30cm 以上的盆", + "notes": "量未指定" + }, + { + "name": "芝麻香油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻香油", + "notes": "量未指定" + }, + { + "name": "单人,约", + "quantity": null, + "unit": null, + "text_quantity": "- 单人,约 20 只", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 200g", + "notes": "量未指定" + }, + { + "name": "冷水", + "quantity": null, + "unit": null, + "text_quantity": "- 冷水 150ml", + "notes": "量未指定" + }, + { + "name": "芝麻香油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻香油 2-3ml", + "notes": "量未指定" + }, + { + "name": "瘦肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 瘦肉末 250g", + "notes": "量未指定" + }, + { + "name": "肥肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 肥肉末 20g #不喜可不加", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 3g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 15g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 2ml", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 2ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 2ml", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "盆中加入所有面粉" + }, + { + "step": 2, + "description": "加入芝麻香油" + }, + { + "step": 3, + "description": "面粉中央挖小洞" + }, + { + "step": 4, + "description": "分 4-5 次加入水,并搅和,当出现碎末状的稍微干燥面团时" + }, + { + "step": 5, + "description": "取消加水,用手将面团压实" + }, + { + "step": 6, + "description": "面团压实至可把盆周围的面粉纳入即可,此步骤为面光盆光" + }, + { + "step": 7, + "description": "将面团置于桌上,盆倒扣于桌上,环境温度为 25 度,使面团醒发约 45 分钟" + }, + { + "step": 8, + "description": "醒发完成后,将面团搓成条状,合成一团,再次搓成条,重复 3 次" + }, + { + "step": 9, + "description": "擀成条状,切成 20 份均匀大小面团,并搓成直径约 3-3.5cm 的球状" + }, + { + "step": 10, + "description": "压扁面团,在手上,桌上,擀面杖上,及面团上撒上面粉,此步骤防止面团发粘" + }, + { + "step": 11, + "description": "用擀面杖将面团擀平,约 8cm 直径,厚约 2mm,中间略微比四周厚 1mm" + }, + { + "step": 12, + "description": "猪肉去皮,保留部分肥肉,切成小块" + }, + { + "step": 13, + "description": "菜刀(建议两把)将猪肉剁成肉沫,放入碗中" + }, + { + "step": 14, + "description": "葱、姜切成末,放入肉碗中搅拌均匀" + }, + { + "step": 15, + "description": "韭菜洗净,切短至 3mm 以下长度" + }, + { + "step": 16, + "description": "韭菜和肉沫混合,加入蚝油、生抽、香油各 2ml,加入一个鸡蛋的蛋清,用手混合搅拌均匀" + }, + { + "step": 17, + "description": "放置 30 分钟即可开始包饺子" + }, + { + "step": 18, + "description": "左手上放面皮,放饺子馅一面尽量不要粘到面粉,防止无法合拢" + }, + { + "step": 19, + "description": "右手用筷子夹约面皮 1/2 直径的馅" + }, + { + "step": 20, + "description": "沿饺子皮圆周进行合拢,捏实,个人吃无需捏花,饺子皮不漏即可" + }, + { + "step": 21, + "description": "使用可放下 20 只饺子的锅,或分批量煮" + }, + { + "step": 22, + "description": "烧水,水约 3/4 锅的高度" + }, + { + "step": 23, + "description": "大火烧开水后放入饺子,调至中火" + }, + { + "step": 24, + "description": "第一次放入饺子,且水冒泡后,锅边加入 50ml 冷水(重复此步骤两次)" + }, + { + "step": 25, + "description": "第三次水开后加入冷水 50ml,水开后调至小火等 60s 即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-汤面", + "name": "汤面的做法", + "description": "# 汤面的做法\n\n汤面是许多人喜爱的基础主食,根据个人喜好加入任何自己喜欢的食材,营养全面,固液兼具,材料易得,做法简单,有手就行。\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/汤面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "菜类材料:建议荤素搭配,选择自己喜欢的食材洗干净即可。例如:", + "quantity": null, + "unit": null, + "text_quantity": "- 菜类材料:建议荤素搭配,选择自己喜欢的食材洗干净即可。例如:", + "notes": "量未指定" + }, + { + "name": "牛羊鱼虾等肉类(生熟皆可)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛羊鱼虾等肉类(生熟皆可)", + "notes": "量未指定" + }, + { + "name": "鸡蛋鸭蛋鹅蛋鸵鸟蛋等蛋类", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋鸭蛋鹅蛋鸵鸟蛋等蛋类", + "notes": "量未指定" + }, + { + "name": "豆块豆筋豆腐皮等豆制品类", + "quantity": null, + "unit": null, + "text_quantity": "- 豆块豆筋豆腐皮等豆制品类", + "notes": "量未指定" + }, + { + "name": "生菜菠菜油麦菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜菠菜油麦菜", + "notes": "量未指定" + }, + { + "name": "青椒番茄胡萝卜等蔬菜类。", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒番茄胡萝卜等蔬菜类。", + "notes": "量未指定" + }, + { + "name": "面类材料:单人一个方便面大小的量,可以在", + "quantity": null, + "unit": null, + "text_quantity": "- 面类材料:单人一个方便面大小的量,可以在 70-230g 之间选择。", + "notes": "量未指定" + }, + { + "name": "冷水: 加入能浸没面的量,一般在", + "quantity": null, + "unit": null, + "text_quantity": "- 冷水: 加入能浸没面的量,一般在 200 - 400 ml 之间选择", + "notes": "量未指定" + }, + { + "name": "菜类:体积大约和面类相当", + "quantity": null, + "unit": null, + "text_quantity": "- 菜类:体积大约和面类相当", + "notes": "量未指定" + }, + { + "name": "其中青菜体积可忽略", + "quantity": null, + "unit": null, + "text_quantity": "- 其中青菜体积可忽略", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "先将菜类材料切成边长不超过 4cm 的块状,便于煮熟" + }, + { + "step": 2, + "description": "如有生肉,则先放入冷水中,盖上锅盖,煮沸腾,先捞出上层血沫,再关火,捞出半熟的肉备用" + }, + { + "step": 3, + "description": "先大火将水加热至沸腾,后调至中火" + }, + { + "step": 4, + "description": "将较难煮熟的食材放入锅中(比如半熟肉类、香菇类、等最先放入锅中)。为保证煮熟,可在沸腾后计时 10 分钟,特别难熟的大块食材可追加 5 分钟。" + }, + { + "step": 5, + "description": "将面食放入锅中,适当搅拌确保面和汤充分接触,使液面保持轻微沸腾,煮 5 分钟。加入面后液面易产生白色泡沫,可适当抬起锅盖通气或者撤下锅盖。" + }, + { + "step": 6, + "description": "将易于煮熟的食材如青菜类放入锅中,适当搅拌以充分浸没,煮 2-5 分钟" + }, + { + "step": 7, + "description": "关火,随后加入盐、胡椒粉、香油等自己喜欢的调味料,适当搅拌即可出锅食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-炒年糕", + "name": "炒年糕的做法", + "description": "# 炒年糕的做法\n\n闽南风味的炒年糕是一道非常好吃的主食。它制作过程简单,原料获取方便,适合海外朋友满足口腹之欲。初学者需要 30 分钟完成,难度较小。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/炒年糕.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "年糕/白粿 (形状不限)", + "quantity": null, + "unit": null, + "text_quantity": "- 年糕/白粿 (形状不限)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "调味料: 酱油,盐", + "quantity": null, + "unit": null, + "text_quantity": "- 调味料: 酱油,盐", + "notes": "量未指定" + }, + { + "name": "(可选):鸡蛋,青菜", + "quantity": null, + "unit": null, + "text_quantity": "- (可选):鸡蛋,青菜", + "notes": "量未指定" + }, + { + "name": "年糕", + "quantity": null, + "unit": null, + "text_quantity": "- 年糕 250 g * 份数", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 2 根 * 份数", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 50 ml", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 15 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1-2g * 份数,按口味喜好。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅中加水烧开,煮熟年糕,碗中加水确保年糕不会粘连,捞起年糕备用。" + }, + { + "step": 2, + "description": "小葱切葱花(将葱白和葱叶分开),青菜切小段备用。" + }, + { + "step": 3, + "description": "(可选) 制作炒蛋,见[西红柿炒蛋](https://github.com/Anduin2017/HowToCook/blob/master/dishes/vegetable_dish/%E8%A5%BF%E7%BA%A2%E6%9F%BF%E7%82%92%E9%B8%A1%E8%9B%8B.md)。" + }, + { + "step": 4, + "description": "热锅,加入 30ml 食用油。" + }, + { + "step": 5, + "description": "将葱白倒入锅中,直至大部分葱白变成焦黄色且发出香味,倒出葱油备用。" + }, + { + "step": 6, + "description": "重新热锅,加入 20ml 食用油。" + }, + { + "step": 7, + "description": "加入所有辅料(鸡蛋,青菜等),翻炒均匀。" + }, + { + "step": 8, + "description": "将年糕的水倒掉,向锅中加入年糕。" + }, + { + "step": 9, + "description": "加入酱油和盐,翻炒均匀。" + }, + { + "step": 10, + "description": "关火,加入葱油,翻炒均匀,乘盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-炒方便面", + "name": "炒方便面的做法", + "description": "# 炒方便面的做法\n\n这是在探究了传统煮方便面的改良方向之后,进行的一次最成功的尝试。它能够让方便面的美味程度提升很大程度,简单好做。开始炒吧!\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/炒方便面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "方便面", + "quantity": null, + "unit": null, + "text_quantity": "- 方便面", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "火腿肠(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿肠(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将火腿肠撕开包装,切成宽度 1cm 的小块。" + }, + { + "step": 2, + "description": "向煮锅中加入 300 ml 水。煮沸。" + }, + { + "step": 3, + "description": "加入方便面面饼,煮 45 秒。煮的过程中将其挑动,把面条打散。" + }, + { + "step": 4, + "description": "面条打散后立刻关火。" + }, + { + "step": 5, + "description": "将面汤和面分离。用凉水冲一下面条。" + }, + { + "step": 6, + "description": "准备一个小碗,将方便面的调料包挤进去。" + }, + { + "step": 7, + "description": "挤进去所有菜包" + }, + { + "step": 8, + "description": "挤进去所有酱包" + }, + { + "step": 9, + "description": "挤进去 50% - 80% 的粉包。(全部粉包都挤进去会很咸)" + }, + { + "step": 10, + "description": "将上一步的面汤取出 80ml,加入小碗,搅匀,得到调料碗。" + }, + { + "step": 11, + "description": "取出计算好的数量的鸡蛋,打入一个小碗。" + }, + { + "step": 12, + "description": "每个鸡蛋加入 2g 盐。搅拌均匀。" + }, + { + "step": 13, + "description": "热锅 20s,加入份数 * 8ml 油。" + }, + { + "step": 14, + "description": "加入刚刚准备好的一碗鸡蛋。翻炒大约 20s 至鸡蛋形成固态即可。" + }, + { + "step": 15, + "description": "将煎鸡蛋取出暂存。" + }, + { + "step": 16, + "description": "热锅 20s,增加锅内的油到份数 * 10ml。" + }, + { + "step": 17, + "description": "加入第一步处理的火腿肠。翻炒 10 秒。" + }, + { + "step": 18, + "description": "加入第二步的面。翻炒 30 秒。" + }, + { + "step": 19, + "description": "加入第三步的调料碗。翻炒 30 秒。" + }, + { + "step": 20, + "description": "加入第四步的煎鸡蛋。翻炒 30 秒。" + }, + { + "step": 21, + "description": "关火盛盘即可。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-炒河粉", + "name": "炒河粉的做法", + "description": "# 炒河粉的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/炒河粉.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "炒河粉、猪肉/牛肉", + "quantity": null, + "unit": null, + "text_quantity": "- 炒河粉、猪肉/牛肉", + "notes": "量未指定" + }, + { + "name": "炒料:盐、味精、老抽、生抽、孜然粉(或直接用河粉料)", + "quantity": null, + "unit": null, + "text_quantity": "- 炒料:盐、味精、老抽、生抽、孜然粉(或直接用河粉料)", + "notes": "量未指定" + }, + { + "name": "其他调味料:胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 其他调味料:胡椒粉", + "notes": "量未指定" + }, + { + "name": "黄瓜、面筋块、绿豆芽、鸡蛋、蒜瓣、小葱、淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜、面筋块、绿豆芽、鸡蛋、蒜瓣、小葱、淀粉", + "notes": "量未指定" + }, + { + "name": "盆、盘子", + "quantity": null, + "unit": null, + "text_quantity": "- 盆、盘子", + "notes": "量未指定" + }, + { + "name": "黄瓜丝", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜丝 30g/人、面筋块 30g/人、绿豆芽 30g/人、打碎的鸡蛋 1 个/人。", + "notes": "量未指定" + }, + { + "name": "拍碎的蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 拍碎的蒜瓣 2 个/人、小葱 1 根/人", + "notes": "量未指定" + }, + { + "name": "河粉料可按", + "quantity": null, + "unit": null, + "text_quantity": "- 河粉料可按 20g/人添加,若自行准备炒料可 10g 盐+2g 味精+3g 孜然粉。", + "notes": "量未指定" + }, + { + "name": "淀粉可准备每", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉可准备每 100g 肉+5g 淀粉比例准备。", + "notes": "量未指定" + }, + { + "name": "老抽/生抽,分别为每", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽/生抽,分别为每 250g 河粉 10ml/15ml。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "小葱切碎(葱白和葱叶分开)、蒜瓣拍碎,丢案板上备用。" + }, + { + "step": 2, + "description": "打碎鸡蛋,捞一点蛋清到一只碗中,剩下的丢入另一只碗中备用。" + }, + { + "step": 3, + "description": "将绿豆芽放入锅中,大火煮 60 秒。豆芽捞出,过凉水,放入盘中备用。" + }, + { + "step": 4, + "description": "黄瓜切丝放入盘中备用,可和豆芽丢一起。" + }, + { + "step": 5, + "description": "处理面筋,单独丢一个盘中。" + }, + { + "step": 6, + "description": "肉切细条状,加入淀粉与刚刚碗中的鸡蛋清、胡椒粉,顺时针拌匀。" + }, + { + "step": 7, + "description": "注:超市购买来的凉皮表面一般会有食用油,可以使用自来水清洗。面筋同样。" + }, + { + "step": 8, + "description": "注:清洗面筋之后,请用手将面筋中的大量水分挤出(不需过于用力)。" + }, + { + "step": 9, + "description": "加入食用油,锅热倒出。" + }, + { + "step": 10, + "description": "倒入处理好的肉,翻炒均匀至变色,倒入碗中备用。" + }, + { + "step": 11, + "description": "趁锅热,加入 20g 食用油(高血压人群可降低用量),倒入葱白、蒜爆炒出香。" + }, + { + "step": 12, + "description": "加入河粉,淋入老抽提色,翻炒均匀后再加入河粉炒料,继续翻炒。" + }, + { + "step": 13, + "description": "河粉即将透明时,放入炒制好的肉丝与面筋,并加入生抽提鲜,简单翻炒两次。" + }, + { + "step": 14, + "description": "加入豆芽与黄瓜丝,翻炒至河粉完全透明。" + }, + { + "step": 15, + "description": "关火!" + }, + { + "step": 16, + "description": "撒入葱叶点缀,把锅端起。" + }, + { + "step": 17, + "description": "倒入盘中,开始干饭。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-炒馍", + "name": "炒馍的做法", + "description": "# 炒馍的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/炒馍.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "馒头(隔天略硬更好)", + "quantity": null, + "unit": null, + "text_quantity": "- 馒头(隔天略硬更好)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "孜然粉", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然粉", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "鸡蛋(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋(可选)", + "notes": "量未指定" + }, + { + "name": "馒头", + "quantity": null, + "unit": null, + "text_quantity": "- 馒头 2 个(隔天略硬更好)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 20ml(花生油或芝麻油更好)", + "notes": "量未指定" + }, + { + "name": "孜然粉", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然粉 3g", + "notes": "量未指定" + }, + { + "name": "辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒粉 3g", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 3g", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 2 棵", + "notes": "量未指定" + }, + { + "name": "鸡蛋 (可选,2 个)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 (可选,2 个)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将馒头切成小块或小片。" + }, + { + "step": 2, + "description": "选有鸡蛋的话将鸡蛋打进碗里,打散(可加盐和五香粉各 1g 或不加,等炒的过程中加)。" + }, + { + "step": 3, + "description": "鸡蛋浇在馒头上,拌匀,鸡蛋不宜过多。" + }, + { + "step": 4, + "description": "大火热锅,倒入食用油(不锈钢锅怕伤锅的话可以先倒油,烧至油热也可也可)" + }, + { + "step": 5, + "description": "将馍丁放进去翻炒,翻炒均匀。" + }, + { + "step": 6, + "description": "将火调小,炒至馍丁呈金黄色。" + }, + { + "step": 7, + "description": "放入盐,胡椒粉,五香粉。" + }, + { + "step": 8, + "description": "最后将葱花放入一起翻炒几下。" + }, + { + "step": 9, + "description": "关火出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-炸酱面", + "name": "炸酱面的做法", + "description": "# 炸酱面的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/炸酱面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "肉丁/肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 肉丁/肉末", + "notes": "量未指定" + }, + { + "name": "面条(挂面或普通面条)", + "quantity": null, + "unit": null, + "text_quantity": "- 面条(挂面或普通面条)", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "菜码(根据个人喜好选择,通常", + "quantity": null, + "unit": null, + "text_quantity": "- 菜码(根据个人喜好选择,通常 4-10 种,可选择黄瓜、白菜、萝卜等)", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "甜面酱", + "quantity": null, + "unit": null, + "text_quantity": "- 甜面酱", + "notes": "量未指定" + }, + { + "name": "肉丁/肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 肉丁/肉末 150g", + "notes": "量未指定" + }, + { + "name": "如果", + "quantity": null, + "unit": null, + "text_quantity": "- 如果 *面条* 选择了*挂面*:150g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 15g", + "notes": "量未指定" + }, + { + "name": "菜码 总量", + "quantity": null, + "unit": null, + "text_quantity": "- 菜码 总量 35g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10g", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 20g", + "notes": "量未指定" + }, + { + "name": "甜面酱", + "quantity": null, + "unit": null, + "text_quantity": "- 甜面酱 20g", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-热干面", + "name": "热干面的做法", + "description": "# 热干面的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/热干面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "热干面特有的碱水面", + "quantity": null, + "unit": null, + "text_quantity": "- 热干面特有的碱水面", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "酸豆角", + "quantity": null, + "unit": null, + "text_quantity": "- 酸豆角", + "notes": "量未指定" + }, + { + "name": "肉末", + "quantity": null, + "unit": null, + "text_quantity": "- 肉末", + "notes": "量未指定" + }, + { + "name": "蒜水", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜水", + "notes": "量未指定" + }, + { + "name": "肉汤汁", + "quantity": null, + "unit": null, + "text_quantity": "- 肉汤汁", + "notes": "量未指定" + }, + { + "name": "萝卜干", + "quantity": null, + "unit": null, + "text_quantity": "- 萝卜干", + "notes": "量未指定" + }, + { + "name": "芝麻酱", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻酱", + "notes": "量未指定" + }, + { + "name": "辣椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "热干面特有的碱水面 (250g)", + "quantity": null, + "unit": null, + "text_quantity": "- 热干面特有的碱水面 (250g)", + "notes": "量未指定" + }, + { + "name": "小葱 (10g)", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 (10g)", + "notes": "量未指定" + }, + { + "name": "酸豆角 (20g)", + "quantity": null, + "unit": null, + "text_quantity": "- 酸豆角 (20g)", + "notes": "量未指定" + }, + { + "name": "肉末 (30g)", + "quantity": null, + "unit": null, + "text_quantity": "- 肉末 (30g)", + "notes": "量未指定" + }, + { + "name": "蒜水 (30ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜水 (30ml)", + "notes": "量未指定" + }, + { + "name": "肉汤汁 (30ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 肉汤汁 (30ml)", + "notes": "量未指定" + }, + { + "name": "萝卜干 (50g)", + "quantity": null, + "unit": null, + "text_quantity": "- 萝卜干 (50g)", + "notes": "量未指定" + }, + { + "name": "芝麻酱 (40ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻酱 (40ml)", + "notes": "量未指定" + }, + { + "name": "辣椒油 (0-10ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油 (0-10ml)", + "notes": "量未指定" + }, + { + "name": "胡椒粉(0-10g)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉(0-10g)", + "notes": "量未指定" + }, + { + "name": "酱油(5ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油(5ml)", + "notes": "量未指定" + }, + { + "name": "食盐(3g)", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐(3g)", + "notes": "量未指定" + }, + { + "name": "鸡精(0-3g)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精(0-3g)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "水煮沸,并加入碱水面,焯烫 25 秒钟捞起" + }, + { + "step": 2, + "description": "撒上食盐、鸡精和胡椒粉" + }, + { + "step": 3, + "description": "芝麻酱用 90ml 水稀释,搅匀,然后加入" + }, + { + "step": 4, + "description": "加入 5ml 酱油,加入 30ml 肉汤汁和蒜水" + }, + { + "step": 5, + "description": "加入萝卜干,肉末,酸豆角,葱花" + }, + { + "step": 6, + "description": "拌均匀后开吃" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-煮泡面加蛋", + "name": "煮泡面加蛋的做法", + "description": "# 煮泡面加蛋的做法\n\n煮泡面加蛋是能满足于各种人群的生存基本需求的重要主食,其材料方便易得,做法简单易上手且制作周期极短。\n\n预估烹饪难度:★", + "source_path": "dishes/staple/煮泡面加蛋.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 1, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "泡面", + "quantity": null, + "unit": null, + "text_quantity": "- 泡面", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "单人,能支撑一个成年人不饥饿状态约", + "quantity": null, + "unit": null, + "text_quantity": "- 单人,能支撑一个成年人不饥饿状态约 3 至 4 小时。", + "notes": "量未指定" + }, + { + "name": "泡面", + "quantity": null, + "unit": null, + "text_quantity": "- 泡面 1 包", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 550ml-1000ml,根据锅的情况。以能完整将泡面浸入其中为准。", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "先将水加热至沸腾(火候不做严格要求,使用热水会更快)" + }, + { + "step": 2, + "description": "将取出的面饼放入锅中" + }, + { + "step": 3, + "description": "将泡面里附带的佐料放入锅中" + }, + { + "step": 4, + "description": "取出筷子轻微拨动泡面,使佐料充分溶解,面饼充分浸泡受热" + }, + { + "step": 5, + "description": "盖上锅盖等待约 1 分钟至锅内水再次沸腾" + }, + { + "step": 6, + "description": "去壳鸡蛋,加入锅中" + }, + { + "step": 7, + "description": "等待约 3 至 4 分钟,即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-老干妈拌面", + "name": "老干妈拌面的做法", + "description": "# 老干妈拌面的做法\n\n预估烹饪难度:★", + "source_path": "dishes/staple/老干妈拌面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 1, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "面", + "quantity": null, + "unit": null, + "text_quantity": "- 面", + "notes": "量未指定" + }, + { + "name": "老干妈", + "quantity": null, + "unit": null, + "text_quantity": "- 老干妈", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 1 升", + "notes": "量未指定" + }, + { + "name": "面量", + "quantity": null, + "unit": null, + "text_quantity": "- 面量 120 克 * 份数", + "notes": "量未指定" + }, + { + "name": "老干妈", + "quantity": null, + "unit": null, + "text_quantity": "- 老干妈 15ml * 份数", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 5ml * 份数", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将水倒入锅中并煮沸" + }, + { + "step": 2, + "description": "将面均匀放入锅中" + }, + { + "step": 3, + "description": "在煮的过程注意搅拌,避免面粘成一坨" + }, + { + "step": 4, + "description": "当用筷子挑起一根面且该面能自然地从筷子上滑落时再等 30 秒关火" + }, + { + "step": 5, + "description": "将面夹入碗中" + }, + { + "step": 6, + "description": "按照上面的计量放入老干妈和酱油" + }, + { + "step": 7, + "description": "用筷子将碗里的面、老干妈、酱油拌均匀" + }, + { + "step": 8, + "description": "吃" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-肉蛋盖饭", + "name": "肉蛋盖饭的做法", + "description": "# 肉蛋盖饭的做法\n\n肉蛋盖饭适合于单人简易晚餐,烹饪大约需要十五分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/肉蛋盖饭.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "肉馅", + "quantity": null, + "unit": null, + "text_quantity": "- 肉馅", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "红葱油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 红葱油(可选)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭 240g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 4 个", + "notes": "量未指定" + }, + { + "name": "肉馅", + "quantity": null, + "unit": null, + "text_quantity": "- 肉馅 300g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 25ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 20ml", + "notes": "量未指定" + }, + { + "name": "红葱油可选", + "quantity": null, + "unit": null, + "text_quantity": "- 红葱油可选 10g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 10g", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 30ml", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 15g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "煮好米饭,通常使用买米赠送的量杯,一杯米 240g" + }, + { + "step": 2, + "description": "锅中放油 30ml" + }, + { + "step": 3, + "description": "放入肉馅,调中火煎至两面微焦" + }, + { + "step": 4, + "description": "将鸡蛋打入锅中,不要打散,盖上锅盖" + }, + { + "step": 5, + "description": "调一个碗汁,碗中放入计算中的对应数量的老抽,生抽,醋,糖,红葱油,搅拌均匀" + }, + { + "step": 6, + "description": "打开锅盖,将碗汁倒入锅中,等待三分钟" + }, + { + "step": 7, + "description": "关火,将肉蛋盖到米饭上" + }, + { + "step": 8, + "description": "安全检查,开始食用盖饭" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-葱油拌面", + "name": "葱油拌面的做法", + "description": "# 葱油拌面的做法\n\n葱油拌面是一道经典的上海家常面点。做法简单,以其独特的葱油香味而闻名。富含碳水化合物和脂肪,能够快速补充能量。一般初学者只需要 20 分钟即可完成。是一道非常适合加班后的简单晚餐选择。\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/葱油拌面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "干面条", + "quantity": null, + "unit": null, + "text_quantity": "- 干面条", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 100 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 100 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 60 ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 20 ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 15 g", + "notes": "量未指定" + }, + { + "name": "干面条", + "quantity": null, + "unit": null, + "text_quantity": "- 干面条 80 g (约相当于 150 g 湿面条)", + "notes": "量未指定" + }, + { + "name": "葱油酱汁", + "quantity": null, + "unit": null, + "text_quantity": "- 葱油酱汁 15 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 小葱 洗净,切成长段(约 5-7 cm)。葱白和葱绿可以分开。" + }, + { + "step": 2, + "description": "锅中加入 100 ml 食用油,中火烧热。先放入葱白段,煸炒至微黄。" + }, + { + "step": 3, + "description": "加入葱绿段,转小火,继续煸炒。" + }, + { + "step": 4, + "description": "保持小火,耐心煸炒约 **15-20 分钟**,直至葱段变得焦黄酥脆。" + }, + { + "step": 5, + "description": "将焦黄的葱段捞出(葱油保留在锅中)。" + }, + { + "step": 6, + "description": "在锅中的葱油中,加入 60 ml 生抽,20 ml 老抽,15 g 白糖。小火加热并搅拌,约 **1 分钟**,至糖溶解,酱汁混合均匀。立即关火。将制作好的葱油酱汁倒入容器中,放凉后密封保存。" + }, + { + "step": 7, + "description": "取 80 g 干面条。" + }, + { + "step": 8, + "description": "锅中加入 1000 ml 饮用水,大火烧开。" + }, + { + "step": 9, + "description": "放入 面条,根据面条包装说明,煮至熟透(通常 **3-8 分钟**,以包装说明为准)。" + }, + { + "step": 10, + "description": "将 煮好的 面条 捞出,沥干水分,放入碗中。" + }, + { + "step": 11, + "description": "在装有 面条 的碗中,加入 15 ml 之前做好的 葱油酱汁。" + }, + { + "step": 12, + "description": "可以加入之前炸好的葱段(可选)。" + }, + { + "step": 13, + "description": "用 筷子 快速搅拌均匀,即可食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-蒸卤面", + "name": "蒸卤面的做法", + "description": "# 蒸卤面的做法\n\n蒸卤面是一道豫南的非常经典的家常菜,荤素搭档,简单易学。一般初学者只需要一个小时即可完成。\n\nNOTE:本次标准为豫南口味,可能和其他地方不太一样,食无标准,兼容并包,好吃即可。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/蒸卤面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "猪五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪五花肉", + "notes": "量未指定" + }, + { + "name": "芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜", + "notes": "量未指定" + }, + { + "name": "葱,姜,蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 葱,姜,蒜", + "notes": "量未指定" + }, + { + "name": "食用油(花生油最佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油(花生油最佳)", + "notes": "量未指定" + }, + { + "name": "生抽,老抽,料酒,盐,五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽,老抽,料酒,盐,五香粉", + "notes": "量未指定" + }, + { + "name": "蒸锅,需带笼屉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸锅,需带笼屉", + "notes": "量未指定" + }, + { + "name": "炒锅", + "quantity": null, + "unit": null, + "text_quantity": "- 炒锅", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒", + "notes": "量未指定" + }, + { + "name": "干红椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干红椒", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "芹菜 两根中等大小的芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜 两根中等大小的芹菜", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 350g", + "notes": "量未指定" + }, + { + "name": "面条", + "quantity": null, + "unit": null, + "text_quantity": "- 面条 500g", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 10cm", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 5 瓣", + "notes": "量未指定" + }, + { + "name": "姜片", + "quantity": null, + "unit": null, + "text_quantity": "- 姜片 20g", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 2 个", + "notes": "量未指定" + }, + { + "name": "干红椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干红椒 3 个", + "notes": "量未指定" + }, + { + "name": "花椒", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒 20 粒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 10g", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 5g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "猪肉去皮,切成 `2 cm * 6 cm * 0.5 cm` 薄片备用" + }, + { + "step": 2, + "description": "芹菜去叶,去掉根部 2cm,然后从中对半切开,切成 2cm 段备用" + }, + { + "step": 3, + "description": "大蒜去皮切粒备用,葱切 0.2cm 薄片备用,姜切细丝备用" + }, + { + "step": 4, + "description": "炒锅烧热至冒烟后,倒入 3ml 食用油滑锅后倒出底油" + }, + { + "step": 5, + "description": "重新加入食用油,加入肉片,葱姜蒜,干红椒,炒 1 分钟,注意不停匀速翻炒" + }, + { + "step": 6, + "description": "加入料酒,生抽,老抽,再翻炒 1 分钟" + }, + { + "step": 7, + "description": "续入 500ml 热水。盖上锅盖炖煮 3 分钟" + }, + { + "step": 8, + "description": "将芹菜,青椒倒入锅中,加入盐,五香粉调味,盖上锅盖继续炖煮煮 3 分钟 后关火" + }, + { + "step": 9, + "description": "蒸锅加入 1000ml 水,烧开上汽后,将面条摊平在笼屉上放入锅中,蒸 15 分钟" + }, + { + "step": 10, + "description": "面条蒸熟后取出,用筷子和无情铁手扒拉散开在案板上,室温冷却" + }, + { + "step": 11, + "description": "将面条放入菜锅中搅拌,搅拌方式为一手持筷,一手持锅铲将菜翻至面条上面,以面条以全部均匀上色为搅拌完成标准" + }, + { + "step": 12, + "description": "将搅拌后的面条再次放在整屉上,再次蒸 10 分钟 关火" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-蛋包饭", + "name": "蛋包饭的做法", + "description": "# 蛋包饭的做法\n\n蛋包饭是一道日式经典家常菜,由炒饭和嫩滑鸡蛋组成,口感丰富,色香味俱全。富含蛋白质、碳水和维生素,是非常适合早餐或正餐的选择。预估制作时间为 25 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/蛋包饭.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋(建议使用土鸡蛋,口感更香)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋(建议使用土鸡蛋,口感更香)", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜", + "notes": "量未指定" + }, + { + "name": "玉米粒", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒", + "notes": "量未指定" + }, + { + "name": "青豆(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 青豆(可选)", + "notes": "量未指定" + }, + { + "name": "火腿肠或鸡胸肉", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿肠或鸡胸肉", + "notes": "量未指定" + }, + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱", + "notes": "量未指定" + }, + { + "name": "食用油(建议使用植物油)", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油(建议使用植物油)", + "notes": "量未指定" + }, + { + "name": "牛奶(可选,让蛋皮更嫩)", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶(可选,让蛋皮更嫩)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 30g", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 30g", + "notes": "量未指定" + }, + { + "name": "火腿肠或鸡胸肉", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿肠或鸡胸肉 50g", + "notes": "量未指定" + }, + { + "name": "玉米粒和青豆总共", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒和青豆总共 30g", + "notes": "量未指定" + }, + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭 200g", + "notes": "量未指定" + }, + { + "name": "番茄酱", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 20ml", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15ml", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 10ml(与鸡蛋混合)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "洋葱、胡萝卜、火腿肠或鸡胸肉切成小丁,备用" + }, + { + "step": 2, + "description": "热锅,锅中倒入 10ml 食用油,等待 10 秒加热" + }, + { + "step": 3, + "description": "先放入洋葱丁翻炒 1 分钟,出香味后加入胡萝卜、玉米粒、青豆继续翻炒 2 分钟" + }, + { + "step": 4, + "description": "加入火腿肠或鸡胸肉丁,炒至变色" + }, + { + "step": 5, + "description": "加入米饭炒散后,加入番茄酱 20ml,翻炒均匀,炒饭完成,盛出备用" + }, + { + "step": 6, + "description": "鸡蛋打散,加入 10ml 牛奶搅匀" + }, + { + "step": 7, + "description": "锅中放入 5ml 食用油,倒入蛋液,轻晃锅底让蛋液均匀铺满锅面" + }, + { + "step": 8, + "description": "用小火加热,待蛋液表面半熟状态时,将炒饭放入蛋液中央" + }, + { + "step": 9, + "description": "用铲子将蛋皮折叠包住米饭,形成椭圆形状" + }, + { + "step": 10, + "description": "用锅铲轻轻推至盘中,整理外形,可在表面挤上少量番茄酱装饰" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-蛋炒饭", + "name": "蛋炒饭的做法", + "description": "# 蛋炒饭的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/蛋炒饭.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "冷饭", + "quantity": null, + "unit": null, + "text_quantity": "- 冷饭", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "火腿", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿", + "notes": "量未指定" + }, + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "灯影牛肉丝/午餐肉/腊肠/卤肉...等熟肉(备选)", + "quantity": null, + "unit": null, + "text_quantity": "- 灯影牛肉丝/午餐肉/腊肠/卤肉...等熟肉(备选)", + "notes": "量未指定" + }, + { + "name": "冷饭(份数*500ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 冷饭(份数*500ml)", + "notes": "量未指定" + }, + { + "name": "鸡蛋 (份数*1.5 //", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 (份数*1.5 // 1 向下取整)", + "notes": "量未指定" + }, + { + "name": "火腿(份数*2 个)", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿(份数*2 个)", + "notes": "量未指定" + }, + { + "name": "黄瓜(可选,份数*30g)", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜(可选,份数*30g)", + "notes": "量未指定" + }, + { + "name": "胡萝卜(可选,份数*30g)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜(可选,份数*30g)", + "notes": "量未指定" + }, + { + "name": "油(份数*12ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 油(份数*12ml)", + "notes": "量未指定" + }, + { + "name": "盐(份数\\*4g - 份数*6g)", + "quantity": null, + "unit": null, + "text_quantity": "- 盐(份数\\*4g - 份数*6g)", + "notes": "量未指定" + }, + { + "name": "胡椒粉(份数*8g)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉(份数*8g)", + "notes": "量未指定" + }, + { + "name": "香葱(份数*1 颗)", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱(份数*1 颗)", + "notes": "量未指定" + }, + { + "name": "生抽(份数*10ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽(份数*10ml)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "米饭提前用铲子铲成小块" + }, + { + "step": 2, + "description": "火腿肠、胡萝卜、黄瓜等根据需求切片或者块状" + }, + { + "step": 3, + "description": "如果家里有熟肉 准备好味道更佳" + }, + { + "step": 4, + "description": "将蛋白,蛋黄分开,分别打入一个大碗里,各自搅匀。注意,不要在这一步加盐。" + }, + { + "step": 5, + "description": "大火热锅,待锅里冒烟放入食用油,放入蛋白,待主体凝固后盛出备用。" + }, + { + "step": 6, + "description": "如果油够,则直接放入蛋黄,如果油不够则放入食用油并等其升温到大火热锅" + }, + { + "step": 7, + "description": "待主体凝固后,将火调至中小火,倒入火腿肠、熟肉,胡萝卜、黄瓜等备料、翻炒 10 秒钟(到爆香)" + }, + { + "step": 8, + "description": "重新倒入蛋白,翻炒 5s 钟,迅速倒入米饭大火翻炒,为的就是每一粒饭都裹上鸡蛋。" + }, + { + "step": 9, + "description": "翻炒过程中将米饭的块状捣碎、这一步过程会比较长、待米饭全部捣碎再翻炒均匀即可" + }, + { + "step": 10, + "description": "调至小火、加盐、胡椒粉、生抽" + }, + { + "step": 11, + "description": "进一步翻炒均匀,能看到一些米饭在锅里有“跳起来”的时候其实就已经差不多了" + }, + { + "step": 12, + "description": "最后倒入香葱再翻炒 10s" + }, + { + "step": 13, + "description": "关火、盛入碗中" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-螺蛳粉", + "name": "螺蛳粉的做法", + "description": "# 螺蛳粉的做法\n\n正宗的螺蛳粉是不臭的!\n\n预估烹饪难度:★", + "source_path": "dishes/staple/螺蛳粉.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 1, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "根据个人经验,一包袋装螺蛳粉足够一人一餐食(虽然看着很大包)", + "quantity": null, + "unit": null, + "text_quantity": "- 根据个人经验,一包袋装螺蛳粉足够一人一餐食(虽然看着很大包)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 1L", + "notes": "量未指定" + }, + { + "name": "袋装螺蛳粉一包,其中应该包含:", + "quantity": null, + "unit": null, + "text_quantity": "- 袋装螺蛳粉一包,其中应该包含:", + "notes": "量未指定" + }, + { + "name": "米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 米粉", + "notes": "量未指定" + }, + { + "name": "螺蛳肉包(可能放在配料包中)", + "quantity": null, + "unit": null, + "text_quantity": "- 螺蛳肉包(可能放在配料包中)", + "notes": "量未指定" + }, + { + "name": "汤料包", + "quantity": null, + "unit": null, + "text_quantity": "- 汤料包", + "notes": "量未指定" + }, + { + "name": "酸笋包、花生包、豆皮包、木耳包等配料包", + "quantity": null, + "unit": null, + "text_quantity": "- 酸笋包、花生包、豆皮包、木耳包等配料包", + "notes": "量未指定" + }, + { + "name": "醋包、辣椒油等调味包", + "quantity": null, + "unit": null, + "text_quantity": "- 醋包、辣椒油等调味包", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅中加水,将水烧开" + }, + { + "step": 2, + "description": "下米粉,煮 3-5 分钟,期间用筷子搅拌,防止米粉粘在一起" + }, + { + "step": 3, + "description": "下汤料包,按个人口味添加" + }, + { + "step": 4, + "description": "下一部分配料包,如木耳,花生,螺蛳(这部分配料需要煮一会才入味)" + }, + { + "step": 5, + "description": "下调味包,按个人口味添加" + }, + { + "step": 6, + "description": "搅拌后捞出,放入碗中" + }, + { + "step": 7, + "description": "下剩下的配料包,如酸笋,豆皮(这部分配料不适合被汤泡太久)" + }, + { + "step": 8, + "description": "享用美食" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-酸辣蕨根粉", + "name": "酸辣蕨根粉的做法", + "description": "# 酸辣蕨根粉的做法\n\n酸辣蕨根粉是一道适合初学者的简单易做的凉菜,可做主食,以酸辣口为主,预计 10 分钟可做完。\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/酸辣蕨根粉.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "蕨根粉", + "quantity": null, + "unit": null, + "text_quantity": "- 蕨根粉", + "notes": "量未指定" + }, + { + "name": "油泼辣子", + "quantity": null, + "unit": null, + "text_quantity": "- 油泼辣子", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "小米辣(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣(可选)", + "notes": "量未指定" + }, + { + "name": "蒜(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜(可选)", + "notes": "量未指定" + }, + { + "name": "葱(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 葱(可选)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "一口有点深度的锅", + "quantity": null, + "unit": null, + "text_quantity": "- 一口有点深度的锅", + "notes": "量未指定" + }, + { + "name": "如果觉得酱料较为清淡,可以加入", + "quantity": null, + "unit": null, + "text_quantity": "- 如果觉得酱料较为清淡,可以加入 2 至 5 克盐", + "notes": "量未指定" + }, + { + "name": "如果想要酱料鲜一些,可以加入", + "quantity": null, + "unit": null, + "text_quantity": "- 如果想要酱料鲜一些,可以加入 2 克糖", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅中加入约为深度 3/5 的水,烧开" + }, + { + "step": 2, + "description": "水沸腾后加入蕨根粉,中小火煮 8 分钟" + }, + { + "step": 3, + "description": "出锅" + }, + { + "step": 4, + "description": "根据配比,加入酱油、醋、油泼辣子" + }, + { + "step": 5, + "description": "用筷子蘸取,尝一口" + }, + { + "step": 6, + "description": "如果觉得此时酱油味稍浓,加入准备好的盐" + }, + { + "step": 7, + "description": "如果觉得此时不够鲜,加入准备好的糖" + }, + { + "step": 8, + "description": "充分搅拌至大部分颗粒状调料溶解" + }, + { + "step": 9, + "description": "取一个碗" + }, + { + "step": 10, + "description": "加入上一步调制的酱料" + }, + { + "step": 11, + "description": "将蕨根粉过冷水后放入酱料中" + }, + { + "step": 12, + "description": "充分搅拌" + }, + { + "step": 13, + "description": "将准备的葱、蒜、小米辣切碎后撒在粉上" + }, + { + "step": 14, + "description": "完成啦(。・∀・)ノ゙" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-醪糟小汤圆", + "name": "醪糟小汤圆的做法", + "description": "# 醪糟小汤圆的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/醪糟小汤圆.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "小汤圆", + "quantity": null, + "unit": null, + "text_quantity": "- 小汤圆", + "notes": "量未指定" + }, + { + "name": "醪糟", + "quantity": null, + "unit": null, + "text_quantity": "- 醪糟", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "枸杞(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 枸杞(可选)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 300 毫升 * 份数", + "notes": "量未指定" + }, + { + "name": "小汤圆", + "quantity": null, + "unit": null, + "text_quantity": "- 小汤圆 250 克 * 份数", + "notes": "量未指定" + }, + { + "name": "醪糟", + "quantity": null, + "unit": null, + "text_quantity": "- 醪糟 50 克 * 份数", + "notes": "量未指定" + }, + { + "name": "枸杞", + "quantity": null, + "unit": null, + "text_quantity": "- 枸杞 5 颗 * 份数", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将水倒入锅中并煮沸" + }, + { + "step": 2, + "description": "放入小汤圆煮 8 分钟" + }, + { + "step": 3, + "description": "放入醪糟和枸杞再煮 2 分钟" + }, + { + "step": 4, + "description": "盛入碗中根据个人口味加入白糖并搅拌均匀" + }, + { + "step": 5, + "description": "吃" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-韭菜盒子", + "name": "韭菜盒子的做法", + "description": "# 韭菜盒子的做法\n\n韭菜盒子是一道美味的传统小吃,外皮酥脆,内馅鲜香,富含维生素和蛋白质。制作简单,适合午餐,预计制作时长约 2.5 小时。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/韭菜盒子.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "韭菜", + "quantity": null, + "unit": null, + "text_quantity": "- 韭菜", + "notes": "量未指定" + }, + { + "name": "虾仁", + "quantity": null, + "unit": null, + "text_quantity": "- 虾仁", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "韭菜", + "quantity": null, + "unit": null, + "text_quantity": "- 韭菜 500g", + "notes": "量未指定" + }, + { + "name": "虾仁", + "quantity": null, + "unit": null, + "text_quantity": "- 虾仁 100g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 3 枚", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 10ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5g", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 250g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将面粉放入大碗中,加入水,搅拌成光滑的面团,静置 30 分钟。" + }, + { + "step": 2, + "description": "韭菜洗净切碎,加入打散的鸡蛋、5g 盐,搅拌均匀。" + }, + { + "step": 3, + "description": "将面团分成小剂子,擀成薄圆饼,包入韭菜、虾仁、鸡蛋液。" + }, + { + "step": 4, + "description": "热锅,加入食用油,放入包好的韭菜盒子,煎至两面金黄,约 3-4 分钟。" + }, + { + "step": 5, + "description": "盛盘,稍凉后即可享用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-麻油拌面", + "name": "麻油拌面的做法", + "description": "# 麻油拌面的做法\n\n省吃俭用懒人的菜:麻油拌面:想必大家都会有节约开销的时刻吧,附上个人耐吃又省钱的食谱。不需要太多的步骤简单的煮,捞,吃。\n\n- 单身的朋友懒惰出门,又不想花钱,简简单单就一餐。\n- 非单身的朋友想存钱,让女友花钱,简简单单就一餐。\n\n预估烹饪难度:★", + "source_path": "dishes/staple/麻油拌面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 1, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "风干快熟面/任何牌子的快熟面(不需要调味料)", + "quantity": null, + "unit": null, + "text_quantity": "- 风干快熟面/任何牌子的快熟面(不需要调味料)", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 1 升", + "notes": "量未指定" + }, + { + "name": "快熟面", + "quantity": null, + "unit": null, + "text_quantity": "- 快熟面 1 块", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油 15ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10 克", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 30 克(可选,这 30g 盐不会被全部食用)", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉 10 克", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 克(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将水倒入锅中并煮沸 (喜欢吃 q 弹面的同学,可在水里加入 30 克盐,用盐水煮出来的面会比较 q 弹)" + }, + { + "step": 2, + "description": "将快熟面放入锅中 3 分钟(也可参考当下品牌快熟面的烹饪时间)" + }, + { + "step": 3, + "description": "当面开始散了可以开始搅拌,让面受热均匀" + }, + { + "step": 4, + "description": "将水滤干把面倒入碗中" + }, + { + "step": 5, + "description": "按照上面的计量放入麻油,老抽,胡椒粉,生抽(可选)" + }, + { + "step": 6, + "description": "筷子搅拌均匀" + }, + { + "step": 7, + "description": "一道简单即省钱的懒人麻油拌面就完成啦" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-麻辣减脂荞麦面", + "name": "麻辣减脂荞麦面的做法", + "description": "# 麻辣减脂荞麦面的做法\n\n麻辣减脂荞麦面做法非常简单,不需要任何厨艺基础。\n一份 298 千卡,美味+便宜+减脂,只需要 20 分钟就可以完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/麻辣减脂荞麦面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "调味料:火锅底料、花生酱、全脂牛奶、生抽、辣椒油、醋、花椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 调味料:火锅底料、花生酱、全脂牛奶、生抽、辣椒油、醋、花椒油", + "notes": "量未指定" + }, + { + "name": "原料:半干荞麦面、娃娃菜、生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 原料:半干荞麦面、娃娃菜、生菜", + "notes": "量未指定" + }, + { + "name": "洗菜盆、直径", + "quantity": null, + "unit": null, + "text_quantity": "- 洗菜盆、直径 18cm 的小锅", + "notes": "量未指定" + }, + { + "name": "半干荞麦面", + "quantity": null, + "unit": null, + "text_quantity": "- 半干荞麦面 100g", + "notes": "量未指定" + }, + { + "name": "娃娃菜", + "quantity": null, + "unit": null, + "text_quantity": "- 娃娃菜 8 片(共 150g)", + "notes": "量未指定" + }, + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜 6 片(共 80g)", + "notes": "量未指定" + }, + { + "name": "火锅底料", + "quantity": null, + "unit": null, + "text_quantity": "- 火锅底料 25g", + "notes": "量未指定" + }, + { + "name": "花生酱", + "quantity": null, + "unit": null, + "text_quantity": "- 花生酱 15g", + "notes": "量未指定" + }, + { + "name": "全脂牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 全脂牛奶 150ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 6ml", + "notes": "量未指定" + }, + { + "name": "辣椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油 10ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 20ml", + "notes": "量未指定" + }, + { + "name": "花椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 花椒油 10ml", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 500ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "娃娃菜、生菜洗好,备用" + }, + { + "step": 2, + "description": "锅内倒入 500ml 水,开大火,将荞麦面和娃娃菜放进去,等待水沸腾" + }, + { + "step": 3, + "description": "水沸腾后,转小火,加入火锅底料、花生酱、牛奶、生抽、辣椒油,水开后煮 5 分钟" + }, + { + "step": 4, + "description": "加入生菜,再煮 2 分钟" + }, + { + "step": 5, + "description": "加入醋、花椒油,关火,直接端着小锅开吃。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-中式馅饼-中式馅饼", + "name": "中式馅饼的做法", + "description": "# 中式馅饼的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/中式馅饼/中式馅饼.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "面粉(非自发粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉(非自发粉)", + "notes": "量未指定" + }, + { + "name": "肉沫", + "quantity": null, + "unit": null, + "text_quantity": "- 肉沫", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "风味调料(如鸡粉、孜然、椒盐,可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 风味调料(如鸡粉、孜然、椒盐,可选)", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "鸡蛋(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋(可选)", + "notes": "量未指定" + }, + { + "name": "胡萝卜(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜(可选)", + "notes": "量未指定" + }, + { + "name": "平底锅", + "quantity": null, + "unit": null, + "text_quantity": "- 平底锅", + "notes": "量未指定" + }, + { + "name": "炒锅(可以使用同一个平底锅替代)", + "quantity": null, + "unit": null, + "text_quantity": "- 炒锅(可以使用同一个平底锅替代)", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 200g", + "notes": "量未指定" + }, + { + "name": "肉沫", + "quantity": null, + "unit": null, + "text_quantity": "- 肉沫 50g", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 30ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 5g", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉 10g", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 5g", + "notes": "量未指定" + }, + { + "name": "风味调料", + "quantity": null, + "unit": null, + "text_quantity": "- 风味调料 3g", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 2 瓣", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 1/4 根(靠叶部分)", + "notes": "量未指定" + }, + { + "name": "鸡蛋 (可选,1 个)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 (可选,1 个)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "取肉沫(解冻),加入 1/2 所有上述调料(油、盐、糖、酱油、风味调料)和全部的生粉,搅拌均匀,腌制 30 分钟。" + }, + { + "step": 2, + "description": "将面粉加入碗中,加入鸡蛋,加入剩下 1/2 所有上述调料,加入相当于面粉 1/2 的水(使得面粉相对粘稠但可以流动),搅拌均匀。" + }, + { + "step": 3, + "description": "蒜头切为蒜末。" + }, + { + "step": 4, + "description": "大葱切段。" + }, + { + "step": 5, + "description": "胡萝卜切末(作为馅料用,所以要求尽量细碎,可用乱刀)" + }, + { + "step": 6, + "description": "热锅冷油,宽油起锅。" + }, + { + "step": 7, + "description": "待油烧热后,放入蒜末爆香。" + }, + { + "step": 8, + "description": "加入腌制的肉沫,翻炒,直至断生。" + }, + { + "step": 9, + "description": "将胡萝卜末加入肉沫中一同翻炒,直至油被染为金黄色(这是为了萃取胡萝卜的风味)。" + }, + { + "step": 10, + "description": "关火。冷却 2 分钟。" + }, + { + "step": 11, + "description": "将炒好的肉沫倒入生面糊中,搅匀。" + }, + { + "step": 12, + "description": "重新开火,平底锅铺底油。" + }, + { + "step": 13, + "description": "调至小火,将面糊倒入锅中均匀铺满。保证厚度不要过高。可以端起锅,让面糊流过锅底来完成这一操作。" + }, + { + "step": 14, + "description": "在饼的表面尚为液态时,撒上大葱段。" + }, + { + "step": 15, + "description": "保持小火,直到底面凝固。" + }, + { + "step": 16, + "description": "将饼翻面,继续小火煎烤,直至另一侧凝固。" + }, + { + "step": 17, + "description": "之后,每一面再额外煎 20 秒。" + }, + { + "step": 18, + "description": "关火出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-凉粉-凉粉", + "name": "凉粉的做法", + "description": "# 凉粉的做法\n\n![liangfen](./lf1.jpg)\n\n伤心凉粉吃了不会让人伤心的哦!\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/凉粉/凉粉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf10.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf11.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf2.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf3.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf4.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf5.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf6.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf7.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf8.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf9.jpg" + ], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "豌豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 豌豆淀粉", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒粉", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "花生碎", + "quantity": null, + "unit": null, + "text_quantity": "- 花生碎", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜", + "notes": "量未指定" + }, + { + "name": "豌豆淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 豌豆淀粉 100g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 3 瓣", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 3 颗", + "notes": "量未指定" + }, + { + "name": "辣椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒粉 10g", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 10ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 10ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 3ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 3g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g", + "notes": "量未指定" + }, + { + "name": "花生碎", + "quantity": null, + "unit": null, + "text_quantity": "- 花生碎 5g", + "notes": "量未指定" + }, + { + "name": "香菜", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "准备食材。" + }, + { + "step": 2, + "description": "把豌豆淀粉和水各 100 克混合搅拌。" + }, + { + "step": 3, + "description": "往锅中倒入 600g 水,大火煮开后转为小火。" + }, + { + "step": 4, + "description": "倒入淀粉水,边倒边不断的搅拌,搅拌到浓稠且色泽均匀。" + }, + { + "step": 5, + "description": "找一个容器,在容器中刷一层薄薄的食用油。" + }, + { + "step": 6, + "description": "将煮好的淀粉倒入容器中冷藏 2-4 小时。" + }, + { + "step": 7, + "description": "冷藏后取出,脱模,切条。" + }, + { + "step": 8, + "description": "大蒜和小米辣剁成沫,放上 10g 辣椒粉,5g 花生碎,热油搅拌均匀。" + }, + { + "step": 9, + "description": "再加入 10ml 酱油,10ml 醋,5g 白糖,3g 鸡精,3g 盐搅拌均匀。" + }, + { + "step": 10, + "description": "将调味料倒在凉粉上,然后撒上香菜即可。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。", + "做法参考:[制作凉粉的详细步骤](https://www.zhms.cn/recipe/mzvyy.html?source=2)" + ] + }, + { + "id": "dishes-staple-基础牛奶面包-基础牛奶面包", + "name": "基础牛奶面包的做法", + "description": "# 基础牛奶面包的做法\n\n![牛奶面包成品](./1-1成品.jpg)\n\n面包是常见的主食。普通面包需要经过长时间的发酵及和面。但本食谱尽量简化了制作步骤,方便新手上手,并尽量保证其风味。当然,要求更高的也可以查阅其的面包食谱。\n\n本食谱**需要的额外的工具较多**,会在后面的章节详细介绍。\n\n本食谱面向**烘焙新手**,难度**中**,预计制作制作时长 **200 分钟**。\n\n预估烹饪难度:★★★★★", + "source_path": "dishes/staple/基础牛奶面包/基础牛奶面包.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/1-1成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/1-1成品.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/2-1设备简介1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/2-2设备简介2.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-1酵头1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-2酵头2.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-3酵头3.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-4此时的面团.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-5转移到容器内.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-6成品面包.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/5-1成品.jpg" + ], + "category": "主食", + "difficulty": 5, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "**酵头**", + "quantity": null, + "unit": null, + "text_quantity": "- **酵头**", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 1 cup (盛一杯后用勺子挂掉多余的部分,不要晃动)", + "notes": "量未指定" + }, + { + "name": "30 ℃ 温水(以不烫手为宜)", + "quantity": null, + "unit": null, + "text_quantity": "- 30 ℃ 温水(以不烫手为宜) 1 cup", + "notes": "量未指定" + }, + { + "name": "酵母", + "quantity": null, + "unit": null, + "text_quantity": "- 酵母 2 g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 g", + "notes": "量未指定" + }, + { + "name": "**面团**", + "quantity": null, + "unit": null, + "text_quantity": "- **面团**", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 2½ cup", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "糖或糖浆 ⅛ cup", + "quantity": null, + "unit": null, + "text_quantity": "- 糖或糖浆 ⅛ cup", + "notes": "量未指定" + }, + { + "name": "奶制品 混合后共 ¼ cup (奶粉需要和水混合。)", + "quantity": null, + "unit": null, + "text_quantity": "- 奶制品 混合后共 ¼ cup (奶粉需要和水混合。)", + "notes": "量未指定" + }, + { + "name": "黄油或玉米油 ⅛ cup", + "quantity": null, + "unit": null, + "text_quantity": "- 黄油或玉米油 ⅛ cup", + "notes": "量未指定" + }, + { + "name": "谷朊粉 ¼ ~ ½ cup (可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 谷朊粉 ¼ ~ ½ cup (可选)", + "notes": "量未指定" + }, + { + "name": "香草精", + "quantity": null, + "unit": null, + "text_quantity": "- 香草精 3 g (可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "*发酵失败了?看看这里:**" + }, + { + "step": 2, + "description": "*如何制作“永久的”酵头?**" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-微波炉腊肠煲仔饭-微波炉腊肠煲仔饭", + "name": "微波炉腊肠煲仔饭的做法", + "description": "# 微波炉腊肠煲仔饭的做法\n\n![微波炉腊肠煲仔饭](微波炉腊肠煲仔饭.png)\n\n程序员以单身汉居多 🐶,做再多的菜也会有一个人吃不完的烦恼,因此一份简单的腊肠煲仔饭则刚刚好。\n\n使用微波炉烹制仅需 `15 分钟` ,既营养又美味,这是一道简单且细腻的主食,给 TA 露上一手吧。\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/微波炉腊肠煲仔饭/微波炉腊肠煲仔饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/微波炉腊肠煲仔饭/微波炉腊肠煲仔饭.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/微波炉腊肠煲仔饭/微波炉腊肠煲仔饭.png" + ], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "工具", + "quantity": null, + "unit": null, + "text_quantity": "- 工具", + "notes": "量未指定" + }, + { + "name": "微波炉", + "quantity": null, + "unit": null, + "text_quantity": "- 微波炉", + "notes": "量未指定" + }, + { + "name": "2 个大碗(推荐微波炉专用碗)", + "quantity": null, + "unit": null, + "text_quantity": "- 2 个大碗(推荐微波炉专用碗)", + "notes": "量未指定" + }, + { + "name": "1 个小碗", + "quantity": null, + "unit": null, + "text_quantity": "- 1 个小碗", + "notes": "量未指定" + }, + { + "name": "原料", + "quantity": null, + "unit": null, + "text_quantity": "- 原料", + "notes": "量未指定" + }, + { + "name": "米", + "quantity": null, + "unit": null, + "text_quantity": "- 米 200 ml", + "notes": "量未指定" + }, + { + "name": "腊肠", + "quantity": null, + "unit": null, + "text_quantity": "- 腊肠 1 根", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "红萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 红萝卜 1 个", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 15 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10 ml", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 1 颗", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将米淘洗干净后倒入 `饭碗` 内,加入 400ml 的水,**盖上盖**" + }, + { + "step": 2, + "description": "放入微波炉,高火,`6` 分钟,煮饭途中准备原料" + }, + { + "step": 3, + "description": "6 分钟后,用毛巾或隔热手套取出碗,可以看见米饭已经八分熟" + }, + { + "step": 4, + "description": "在米饭上摆入切片的腊肠,继续高火 `2` 分钟" + }, + { + "step": 5, + "description": "取出腊肠饭,放入 `青菜碗`,高火 `4-5` 分钟" + }, + { + "step": 6, + "description": "在腊肠饭上摆好青菜,磕入鸡蛋,看个人喜好继续高火 `40-60` 秒" + }, + { + "step": 7, + "description": "取出腊肠饭,此时已经基本完成。" + }, + { + "step": 8, + "description": "将 `小碗` 放入,继续高火 `30` 秒" + }, + { + "step": 9, + "description": "在腊肠饭上淋上叮热的生抽,撒上葱花即可" + }, + { + "step": 10, + "description": "多余的青菜可以沾着酱油吃" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-意式肉酱面-意式肉酱面", + "name": "意式肉酱面的做法", + "description": "# 意式肉酱面的做法\n\n![示例菜成品](./final.jpg)\n\n意式肉酱面是一道非常容易做的菜,做得熟练的话,可以在 15 分钟内完成,从此告别方便面\n\n预估烹饪难度:★", + "source_path": "dishes/staple/意式肉酱面/意式肉酱面.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/final.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/final.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/sauce.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/spaghetti.jpg" + ], + "category": "主食", + "difficulty": 1, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "意大利面", + "quantity": null, + "unit": null, + "text_quantity": "- 意大利面", + "notes": "量未指定" + }, + { + "name": "意大利面酱", + "quantity": null, + "unit": null, + "text_quantity": "- 意大利面酱", + "notes": "量未指定" + }, + { + "name": "肉沫", + "quantity": null, + "unit": null, + "text_quantity": "- 肉沫", + "notes": "量未指定" + }, + { + "name": "白洋葱(紫洋葱也可以)", + "quantity": null, + "unit": null, + "text_quantity": "- 白洋葱(紫洋葱也可以)", + "notes": "量未指定" + }, + { + "name": "意大利面", + "quantity": null, + "unit": null, + "text_quantity": "- 意大利面 180 克(可以根据食量上下浮动)", + "notes": "量未指定" + }, + { + "name": "肉沫", + "quantity": null, + "unit": null, + "text_quantity": "- 肉沫 80 克(可以根据食量上下浮动)", + "notes": "量未指定" + }, + { + "name": "洋葱大半个 (大约", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱大半个 (大约 150 克,通常是肉的两倍重)", + "notes": "量未指定" + }, + { + "name": "意大利面酱", + "quantity": null, + "unit": null, + "text_quantity": "- 意大利面酱 300 克(可以看情况上下浮动)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "锅中加水,烧开后放入意面(等待 6 - 12 分钟)" + }, + { + "step": 2, + "description": "在烧水的时候可以进行下面这些步骤,但请注意煮面的时间" + }, + { + "step": 3, + "description": "洋葱切成小丁" + }, + { + "step": 4, + "description": "空锅中倒油,中火下入洋葱碎" + }, + { + "step": 5, + "description": "时刻搅拌,注意不要让洋葱烧糊,直到洋葱变成半透明状" + }, + { + "step": 6, + "description": "下入肉沫,继续搅拌(搅散),直到肉末变成棕色" + }, + { + "step": 7, + "description": "加入意大利面酱,稍微搅拌一下即可" + }, + { + "step": 8, + "description": "把煮好的意大利面沥干水分并倒入肉酱中搅拌均匀即可(或者直接把做好的肉酱倒在意面上也行)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-扬州炒饭-扬州炒饭", + "name": "扬州炒饭的做法", + "description": "# 扬州炒饭的做法\n\n扬州炒饭是蛋炒饭的升级版,制作时间较长,但是制作步骤简单\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/扬州炒饭/扬州炒饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/扬州炒饭/veg.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/扬州炒饭/veg.png" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "冷饭(干一点的为佳)", + "quantity": null, + "unit": null, + "text_quantity": "- 冷饭(干一点的为佳)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "冷冻去皮基围虾", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻去皮基围虾", + "notes": "量未指定" + }, + { + "name": "午餐肉罐头", + "quantity": null, + "unit": null, + "text_quantity": "- 午餐肉罐头", + "notes": "量未指定" + }, + { + "name": "青豆", + "quantity": null, + "unit": null, + "text_quantity": "- 青豆", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜", + "notes": "量未指定" + }, + { + "name": "玉米粒(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒(可选)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "冷饭", + "quantity": null, + "unit": null, + "text_quantity": "- 冷饭 500g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2-3 个", + "notes": "量未指定" + }, + { + "name": "冷冻去头去皮基围虾", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻去头去皮基围虾 10-15 只", + "notes": "量未指定" + }, + { + "name": "午餐肉罐头", + "quantity": null, + "unit": null, + "text_quantity": "- 午餐肉罐头 150g(推荐上海梅林的火腿午餐肉罐头,340g 每罐,一次用半罐)", + "notes": "量未指定" + }, + { + "name": "青豆", + "quantity": null, + "unit": null, + "text_quantity": "- 青豆 30g", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 30g", + "notes": "量未指定" + }, + { + "name": "玉米粒", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒 30g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1 根", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 30-40ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 12-15g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "胡萝卜切丁 0.2cm*0.2cm*0.2cm,备用" + }, + { + "step": 2, + "description": "午餐肉切丁 0.2cm*0.2cm*0.2cm,备用" + }, + { + "step": 3, + "description": "葱分别取葱白和葱绿,各切成 0.25-0.5cm 的小段,分开备用" + }, + { + "step": 4, + "description": "在碗中打入鸡蛋液,均匀搅拌,备用" + }, + { + "step": 5, + "description": "将胡萝卜,青豆,玉米粒煮熟捞出,备用(水别倒)" + }, + { + "step": 6, + "description": "将虾煮熟,捞出备用(水可以倒了)" + }, + { + "step": 7, + "description": "热锅热油,可以参考[学习炒与煎](../../../tips/learn/学习炒与煎.md)中的热锅双油" + }, + { + "step": 8, + "description": "鸡蛋凝固后立刻捞出,备用" + }, + { + "step": 9, + "description": "将午餐肉,青豆,胡萝卜,玉米粒,虾倒入锅中翻炒 1-2 分钟,装盘备用" + }, + { + "step": 10, + "description": "水冲一下锅,将杂物冲干净,保证锅内干净(可以有油但是不能有杂质)" + }, + { + "step": 11, + "description": "热锅热油(10ml),将葱白放入爆香" + }, + { + "step": 12, + "description": "调至小火(如果油温过高可以关火 1-2 分钟),放入米饭,用铲子快速砸击米饭并翻炒,保证米饭均匀沾到油且粒粒分明" + }, + { + "step": 13, + "description": "倒入鸡蛋,继续砸击,使鸡蛋碎开并与米饭充分混合" + }, + { + "step": 14, + "description": "转大火,倒入其他所有备用配料,快速翻炒 1-2 分钟" + }, + { + "step": 15, + "description": "撒入盐,并翻炒至充分混合" + }, + { + "step": 16, + "description": "撒入葱绿,翻炒 1 分钟" + }, + { + "step": 17, + "description": "关火,装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-披萨饼皮-披萨饼皮", + "name": "披萨饼皮的做法", + "description": "# 披萨饼皮的做法\n\n![示例是青红椒火腿披萨](./001.jpeg)\n\n披萨制作总体来说比较简单,稍微有点麻烦也是争议最多的就是披萨饼皮,做好了披萨饼皮喜欢吃什么口味的披萨,直接把准备好的食材放上去烤熟就好,所以这里重点说一下披萨饼皮如何制作。\n\n本教程中的饼皮是属于软面团低温隔夜发酵\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/披萨饼皮/披萨饼皮.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/披萨饼皮/001.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/披萨饼皮/001.jpeg" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "*原料**", + "quantity": null, + "unit": null, + "text_quantity": "- *原料**", + "notes": "量未指定" + }, + { + "name": "中筋面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 中筋面粉", + "notes": "量未指定" + }, + { + "name": "水(温水)", + "quantity": null, + "unit": null, + "text_quantity": "- 水(温水)", + "notes": "量未指定" + }, + { + "name": "安琪干酵母粉", + "quantity": null, + "unit": null, + "text_quantity": "- 安琪干酵母粉", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "*工具**", + "quantity": null, + "unit": null, + "text_quantity": "- *工具**", + "notes": "量未指定" + }, + { + "name": "烤箱", + "quantity": null, + "unit": null, + "text_quantity": "- 烤箱", + "notes": "量未指定" + }, + { + "name": "烘焙油纸", + "quantity": null, + "unit": null, + "text_quantity": "- 烘焙油纸", + "notes": "量未指定" + }, + { + "name": "披萨石(有更好,没有普通烤盘也可以)", + "quantity": null, + "unit": null, + "text_quantity": "- 披萨石(有更好,没有普通烤盘也可以)", + "notes": "量未指定" + }, + { + "name": "擀面杖(非必需)", + "quantity": null, + "unit": null, + "text_quantity": "- 擀面杖(非必需)", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 125g x 4= 500g,", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 70 x 5 = 350g,", + "notes": "量未指定" + }, + { + "name": "橄榄油", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄油 7 x 5 =35g,", + "notes": "量未指定" + }, + { + "name": "酵母粉", + "quantity": null, + "unit": null, + "text_quantity": "- 酵母粉 1 x 5 = 5g,", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 0.6 x 5 = 3g,", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 0.6 x 5 = 3g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "用准备好的温水把酵母粉化开,稍微搅拌小就好,备用" + }, + { + "step": 2, + "description": "取准备好的面粉,依次添加盐、橄榄油、白砂糖" + }, + { + "step": 3, + "description": "准备混合水和面粉,边加水边搅拌直至水全部加完" + }, + { + "step": 4, + "description": "搅拌至看不到干米粉为止" + }, + { + "step": 5, + "description": "用差不多三倍大面团的容器装好,密封,冰箱冷藏(4 度) **等待 8~12 小时,一般晚上做第二天就可以用**" + }, + { + "step": 6, + "description": "观察面团醒发完毕 **差不多是原始大小大约两倍算醒发完毕**" + }, + { + "step": 7, + "description": "取醒发好的面团,均匀分成四份,分别用保鲜膜盖好,备用" + }, + { + "step": 8, + "description": "案板撒稍微多一点的干面粉,准备开始揉面" + }, + { + "step": 9, + "description": "因为是比较湿的面团,所以粘上干面粉后才没那么粘手,不用揉太多次,面团表面稍微光滑一点就可以了" + }, + { + "step": 10, + "description": "用手拉扯,或者擀面杖擀平,也不一定非得擀圆,只要厚度均匀,烤箱放得进去就好" + }, + { + "step": 11, + "description": "铺好油纸,放上饼皮,依照个人口味,把准备好的食材放上去,撒上芝士碎" + }, + { + "step": 12, + "description": "水果烤箱上 180 度,下 220 度,16 分钟即可" + }, + { + "step": 13, + "description": "肉蔬菜烤箱上 200 度,下 230 度,18 分钟即可" + }, + { + "step": 14, + "description": "挤上沙拉酱或者其他自己喜欢的酱即可享用~" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-日式咖喱饭-日式咖喱饭", + "name": "日式咖喱饭的做法", + "description": "# 日式咖喱饭的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/日式咖喱饭/日式咖喱饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式咖喱饭/成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式咖喱饭/成品.jpg" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 2 个", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 2 个", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 1 根", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 2~3 瓣", + "notes": "量未指定" + }, + { + "name": "肉", + "quantity": null, + "unit": null, + "text_quantity": "- 肉 2 斤", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "胡萝卜去头尾,去皮,滚刀切" + }, + { + "step": 2, + "description": "洋葱剥去外层去芯,切成月牙状" + }, + { + "step": 3, + "description": "土豆去皮、切大块" + }, + { + "step": 4, + "description": "肉切块状" + }, + { + "step": 5, + "description": "剥蒜拍平切碎" + }, + { + "step": 6, + "description": "咖喱块切碎,增加接触面积加速溶解" + }, + { + "step": 7, + "description": "热油锅放入蒜和肉,**快速翻炒**至肉*表面变白*" + }, + { + "step": 8, + "description": "加入胡萝卜,**快速翻炒**至均匀受热" + }, + { + "step": 9, + "description": "加入洋葱,**快速翻炒**至洋葱*变透明状*" + }, + { + "step": 10, + "description": "加入土豆,保持翻炒至土豆*变软*(可以用筷子确认)" + }, + { + "step": 11, + "description": "加水没过所有食材,沸腾后**等待 15 分钟**" + }, + { + "step": 12, + "description": "关火,加咖喱并搅拌" + }, + { + "step": 13, + "description": "等待咖喱融化后再开火,缓慢**搅拌 10 分钟**,防止糊锅" + }, + { + "step": 14, + "description": "在外观*呈粘稠状态*关火结束制作" + }, + { + "step": 15, + "description": "微波炉:单人份高火 2-3 分钟" + }, + { + "step": 16, + "description": "锅:需额外加 50ml 水,加热时保持搅拌" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-日式肥牛丼饭-日式肥牛丼饭", + "name": "日式肥牛丼饭的做法", + "description": "# 日式肥牛丼饭的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/日式肥牛丼饭/日式肥牛丼饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式肥牛丼饭/成品.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式肥牛丼饭/成品.png" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 1 个", + "notes": "量未指定" + }, + { + "name": "肥牛", + "quantity": null, + "unit": null, + "text_quantity": "- 肥牛 250 克", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1~2 根", + "notes": "量未指定" + }, + { + "name": "白芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 白芝麻 5 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "洋葱剥去外层去芯,切成月牙状" + }, + { + "step": 2, + "description": "葱洗净切成 0.5cm 的小段" + }, + { + "step": 3, + "description": "热锅直接放入白芝麻,**前后晃动锅体**使芝麻均匀受热至*略呈金黄色*" + }, + { + "step": 4, + "description": "肥牛焯水 1 分钟后捞出" + }, + { + "step": 5, + "description": "将 40g `味淋`(或 30g `料酒`),30g `酱油`,20g `耗油`,5g `糖`,5g `老抽`(可选,用于调色),在碗中搅拌混合成`调料`(该步骤可直接将碗放在电子秤上进行)" + }, + { + "step": 6, + "description": "热油锅放入洋葱,**快速翻炒**至洋葱*变透明状*" + }, + { + "step": 7, + "description": "关小火,加入 250g 水(或出汁),开回大火加热**等待 3 分钟**" + }, + { + "step": 8, + "description": "加入牛肉和`调料`" + }, + { + "step": 9, + "description": "**不断翻动**所有食材 **10 分钟**,防止食材粘锅" + }, + { + "step": 10, + "description": "关火" + }, + { + "step": 11, + "description": "盛装肥牛丼至[米饭](../米饭/电饭煲蒸米饭.md)上(注意要把汁水淋一些在饭上)" + }, + { + "step": 12, + "description": "撒上葱花和白芝麻,制作完成。" + }, + { + "step": 13, + "description": "微波炉:单人份高火 2-3 分钟" + }, + { + "step": 14, + "description": "锅:需额外加 50ml 水,加热时需**不断翻动**" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-河南蒸面条-河南蒸面条", + "name": "河南蒸面条的做法", + "description": "# 河南蒸面条的做法\n\n![示例菜成品](./河南蒸面条.png)\n\n河南蒸面条是一道在河南坊间流行的小吃,也可以用家里的挂面制作。\n\n简单来讲,是先将挂面裹油放入蒸笼蒸熟,再加蔬菜配以调料炒,最后二次蒸制,以达到入味劲道的效果。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/河南蒸面条/河南蒸面条.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/河南蒸面条/河南蒸面条.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/河南蒸面条/河南蒸面条.png" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "挂面 (推荐圆的)", + "quantity": null, + "unit": null, + "text_quantity": "- 挂面 (推荐圆的)", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "蒜薹", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜薹", + "notes": "量未指定" + }, + { + "name": "葱 + 姜 + 蒜 + 料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 + 姜 + 蒜 + 料酒", + "notes": "量未指定" + }, + { + "name": "盐 + 鸡精 + 十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 + 鸡精 + 十三香", + "notes": "量未指定" + }, + { + "name": "生抽 + 老抽 + 蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 + 老抽 + 蚝油", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油", + "notes": "量未指定" + }, + { + "name": "油 + 锅 + 菜刀 + 铲子", + "quantity": null, + "unit": null, + "text_quantity": "- 油 + 锅 + 菜刀 + 铲子", + "notes": "量未指定" + }, + { + "name": "蒸篦子", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸篦子", + "notes": "量未指定" + }, + { + "name": "额外的盆", + "quantity": null, + "unit": null, + "text_quantity": "- 额外的盆", + "notes": "量未指定" + }, + { + "name": "挂面", + "quantity": null, + "unit": null, + "text_quantity": "- 挂面 300g", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 350g", + "notes": "量未指定" + }, + { + "name": "蒜薹", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜薹 150g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 2g", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香 1g", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 10g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 5g", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 10g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 5ml", + "notes": "量未指定" + }, + { + "name": "麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 麻油 5ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "起锅加 7 成水,水开,上蒸篦子" + }, + { + "step": 2, + "description": "将挂面,均匀铺开放置,淋 5ml 油并抹匀,蒸 15 分钟" + }, + { + "step": 3, + "description": "将挂面和蒸篦子取出,放置一边,并倒掉锅中的水" + }, + { + "step": 4, + "description": "五花肉,切成 2mm 厚度的肉片" + }, + { + "step": 5, + "description": "蒜薹,切成 3cm 段" + }, + { + "step": 6, + "description": "葱,切成 0.2cm 薄片" + }, + { + "step": 7, + "description": "姜,切成 1mm x 1mm x 3cm 的细丝" + }, + { + "step": 8, + "description": "蒜,放在砧板上拍碎,切成 1mm 的粒度" + }, + { + "step": 9, + "description": "起锅,烧干水分,加 3ml 食用油" + }, + { + "step": 10, + "description": "手持锅柄,摇晃锅,使食用油充分挂满锅的 2/3" + }, + { + "step": 11, + "description": "中火,加入肉片,翻炒 1 分钟" + }, + { + "step": 12, + "description": "加入葱姜蒜,料酒,继续翻炒 1 分钟" + }, + { + "step": 13, + "description": "将蒜薹段,放入锅中,翻炒 1 分钟" + }, + { + "step": 14, + "description": "开始调味,加入老抽、生抽、蚝油、盐、鸡精、十三香,翻炒 1 分钟" + }, + { + "step": 15, + "description": "加入 500ML 水,没过蔬菜,炖煮 1 分钟" + }, + { + "step": 16, + "description": "将蒸好的挂面放入,不断搅拌 3 分钟,待挂面全部均匀上色,关火" + }, + { + "step": 17, + "description": "将搅拌好的挂面和菜,全部倒入额外的盆中" + }, + { + "step": 18, + "description": "起锅,加冷水 7 成,放上蒸篦子,将拌好的面条和菜,均匀的铺在上面" + }, + { + "step": 19, + "description": "水开后,大火烧 15 分钟,出锅" + }, + { + "step": 20, + "description": "淋上 10g 的麻油,即可食用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-火腿饭团-火腿饭团", + "name": "火腿饭团的做法", + "description": "# 火腿饭团的做法\n\n![火腿饭团](./饭团.png)\n好吃!富含碳水和蛋白质还有维生素。有手就行的制作难度,预计制作时间 1 h 。\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/火腿饭团/火腿饭团.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/火腿饭团/饭团.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/火腿饭团/饭团.png" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "火腿", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿", + "notes": "量未指定" + }, + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "冷冻青豆(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻青豆(可选)", + "notes": "量未指定" + }, + { + "name": "冷冻玉米粒(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻玉米粒(可选)", + "notes": "量未指定" + }, + { + "name": "海苔碎(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 海苔碎(可选)", + "notes": "量未指定" + }, + { + "name": "喜欢的沙拉酱(推荐日式 mayo!)", + "quantity": null, + "unit": null, + "text_quantity": "- 喜欢的沙拉酱(推荐日式 mayo!)", + "notes": "量未指定" + }, + { + "name": "火腿(100g)", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿(100g)", + "notes": "量未指定" + }, + { + "name": "米饭(125g)", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭(125g)", + "notes": "量未指定" + }, + { + "name": "水(90ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 水(90ml)", + "notes": "量未指定" + }, + { + "name": "冷冻青豆(30g)", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻青豆(30g)", + "notes": "量未指定" + }, + { + "name": "冷冻玉米粒(30g)", + "quantity": null, + "unit": null, + "text_quantity": "- 冷冻玉米粒(30g)", + "notes": "量未指定" + }, + { + "name": "海苔碎(10g)", + "quantity": null, + "unit": null, + "text_quantity": "- 海苔碎(10g)", + "notes": "量未指定" + }, + { + "name": "喜欢的沙拉酱(20g)", + "quantity": null, + "unit": null, + "text_quantity": "- 喜欢的沙拉酱(20g)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将米饭和水放到电饭锅里,点击米饭模式,等待完成" + }, + { + "step": 2, + "description": "冷冻玉米粒和青豆放到锅里,加水没过所有食材,沸腾后静待 2 分钟后,捞出。" + }, + { + "step": 3, + "description": "火腿切成 1cm 的方块" + }, + { + "step": 4, + "description": "与此同时,加入 10ml 食用油,加入火腿翻炒至火腿上色" + }, + { + "step": 5, + "description": "将米饭,火腿,海苔碎,青豆,玉米粒,沙拉酱放入碗中,混合均匀即可" + }, + { + "step": 6, + "description": "装盘(如果有的话)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-炒凉粉-炒凉粉", + "name": "炒凉粉的做法", + "description": "# 炒凉粉的做法\n\n![炒凉粉成品](./chaoliangfen.jpg)\n\n炒凉粉是一道流行于山西、陕西地区的一道特色小吃,入口滑嫩,老少皆宜。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/炒凉粉/炒凉粉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒凉粉/chaoliangfen.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒凉粉/chaoliangfen.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒凉粉/炒凉粉成品.jpg" + ], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "凉粉", + "quantity": null, + "unit": null, + "text_quantity": "- 凉粉", + "notes": "量未指定" + }, + { + "name": "玉米油", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米油", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香", + "notes": "量未指定" + }, + { + "name": "中粗辣椒面", + "quantity": null, + "unit": null, + "text_quantity": "- 中粗辣椒面", + "notes": "量未指定" + }, + { + "name": "矿泉水", + "quantity": null, + "unit": null, + "text_quantity": "- 矿泉水", + "notes": "量未指定" + }, + { + "name": "凉粉", + "quantity": null, + "unit": null, + "text_quantity": "- 凉粉 500g", + "notes": "量未指定" + }, + { + "name": "玉米油", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米油 10ml", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末 10g", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 15g", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱 15g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 20ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 10ml", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 5g", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香 5g", + "notes": "量未指定" + }, + { + "name": "中粗辣椒面", + "quantity": null, + "unit": null, + "text_quantity": "- 中粗辣椒面 15g", + "notes": "量未指定" + }, + { + "name": "矿泉水", + "quantity": null, + "unit": null, + "text_quantity": "- 矿泉水 20ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "凉粉改刀切麻将块大小" + }, + { + "step": 2, + "description": "开小火,起锅烧油,锅烧微热后,下入蒜末爆香后加入豆瓣酱炒出红油" + }, + { + "step": 3, + "description": "将凉粉块下入锅中,翻炒 10 秒" + }, + { + "step": 4, + "description": "加入生抽提味,老抽上色,翻炒均匀后加入辣椒面继续翻炒均匀" + }, + { + "step": 5, + "description": "加入食盐、十三香继续翻炒 10 秒" + }, + { + "step": 6, + "description": "加入准备好的矿泉水,再次翻炒 10 秒,待汤汁浓稠后,关火出锅装盘" + }, + { + "step": 7, + "description": "撒上葱花即可完成" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-炒意大利面-炒意大利面", + "name": "炒意大利面的做法", + "description": "# 炒意大利面的做法\n\n![意大利面](./a.jpg)\n\n这是一道软糯爽口的意大利面的做法,非常简单,用时大概 30 分钟。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/炒意大利面/炒意大利面.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒意大利面/a.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒意大利面/a.jpg" + ], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "意大利面", + "quantity": null, + "unit": null, + "text_quantity": "- 意大利面", + "notes": "量未指定" + }, + { + "name": "肥牛片", + "quantity": null, + "unit": null, + "text_quantity": "- 肥牛片", + "notes": "量未指定" + }, + { + "name": "番茄酱 / 黑胡椒酱(选其一即可)", + "quantity": null, + "unit": null, + "text_quantity": "- 番茄酱 / 黑胡椒酱(选其一即可)", + "notes": "量未指定" + }, + { + "name": "菜籽油(其他植物油也可)", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油(其他植物油也可)", + "notes": "量未指定" + }, + { + "name": "意大利面", + "quantity": null, + "unit": null, + "text_quantity": "- 意大利面 50 克 / 人", + "notes": "量未指定" + }, + { + "name": "肥牛", + "quantity": null, + "unit": null, + "text_quantity": "- 肥牛 5 片 / 人", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 5ml / 50 克意面", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "加入 250 克水 / 人" + }, + { + "step": 2, + "description": "待水烧开,下入面条,中火煮 15 - 20 分钟(这个面通常比较硬,捞起来之前最好尝一下,中心如果有一点硬,需要继续煮)" + }, + { + "step": 3, + "description": "捞出面条,盛入盘中备用" + }, + { + "step": 4, + "description": "热锅倒入食用油,待油温中热,下入面条翻炒一分钟(如果太干,加入少量水)" + }, + { + "step": 5, + "description": "放入 10 克番茄酱、肥牛、加入 2g 食盐,继续翻炒一分钟" + }, + { + "step": 6, + "description": "起锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-烙饼-烙饼", + "name": "烙饼的做法", + "description": "# 烙饼的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/烙饼/烙饼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烙饼/成品.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烙饼/成品.jpg" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "电饼铛", + "quantity": null, + "unit": null, + "text_quantity": "- 电饼铛", + "notes": "量未指定" + }, + { + "name": "面粉 =", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 = 400g", + "notes": "量未指定" + }, + { + "name": "热水 =", + "quantity": null, + "unit": null, + "text_quantity": "- 热水 = 130ml(80 度)", + "notes": "量未指定" + }, + { + "name": "冷水 =", + "quantity": null, + "unit": null, + "text_quantity": "- 冷水 = 130ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 400g 面粉倒入盆中,一半用凉水和面,一半用热水和面,搅拌成面絮,用手揉成团。用保鲜膜封起来,醒面 40 分钟" + }, + { + "step": 2, + "description": "离醒面完成时间还有 10 分钟时,请查看[小技巧](../../condiment/油酥.md)中的油酥做法(热油酥效果更好)" + }, + { + "step": 3, + "description": "醒好的面不用揉,稍微摁一下,用一横刀一竖刀将其分成四份。" + }, + { + "step": 4, + "description": "搓圆,擀开,擀成与电饼铛大小差不多的饼,取 1/4 的油酥,将饼表面涂抹均匀" + }, + { + "step": 5, + "description": "沿饼的半径切开,从外圈将其卷成圆锥形,然后将圆锥尾部捏好,防止油酥外漏。" + }, + { + "step": 6, + "description": "按压面饼圆锥尖的地方,将其压扁,然后再次擀成与电饼铛大小差不多的面饼(厚度约为 3mm)" + }, + { + "step": 7, + "description": "将电饼铛预热,涂上凉油(热锅凉油),将擀好的饼放入电饼铛中,将饼的上方也刷点油,涂抹均匀(锁住水分),盖上盖子" + }, + { + "step": 8, + "description": "大火烙一分钟,打开盖子,将饼翻个面再烙一分钟" + }, + { + "step": 9, + "description": "重复以上动作,完成饼的烙制" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-烧饼-芝麻烧饼", + "name": "芝麻烧饼的做法", + "description": "# 芝麻烧饼的做法\n\n![示例菜成品](./芝麻烧饼.jpg)\n芝麻烧饼,外酥里软,简单易做。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/烧饼/芝麻烧饼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烧饼/芝麻烧饼.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烧饼/芝麻烧饼.jpg" + ], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "酵母粉", + "quantity": null, + "unit": null, + "text_quantity": "- 酵母粉", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "温水(", + "quantity": null, + "unit": null, + "text_quantity": "- 温水( 40℃ )", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "面团:300 克面粉,3 克酵母粉,3 克白糖,180 克温水,20 克食用油,醒面 10 分钟" + }, + { + "step": 2, + "description": "油酥:小碗放 30 克面粉,2 克盐,4 克十三香,20 克食用油,拌匀后,静置" + }, + { + "step": 3, + "description": "做饼:面擀成长方形,抹上调好的油酥,从一头卷起,切成 7 个面剂子,对折,用虎口收拢即可,先沾水再沾白芝麻,擀成小圆饼" + }, + { + "step": 4, + "description": "烙饼:将电饼铛预热,倒入凉油(锅底铺满油),将擀好的饼放入电饼铛中,将饼的上方也刷点油,涂抹均匀盖上盖子,选大饼档,听到叮的一声出锅即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-电饭煲三文鱼炊饭-电饭煲三文鱼炊饭", + "name": "电饭煲三文鱼炊饭的做法", + "description": "# 电饭煲三文鱼炊饭的做法\n\n![电饭煲三文鱼炊饭示例菜成品](./电饭煲三文鱼炊饭.webp)\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/电饭煲三文鱼炊饭/电饭煲三文鱼炊饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/电饭煲三文鱼炊饭/电饭煲三文鱼炊饭.webp", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/电饭煲三文鱼炊饭/电饭煲三文鱼炊饭.webp" + ], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "有盐牛油", + "quantity": null, + "unit": null, + "text_quantity": "- 有盐牛油", + "notes": "量未指定" + }, + { + "name": "三文鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 三文鱼", + "notes": "量未指定" + }, + { + "name": "米", + "quantity": null, + "unit": null, + "text_quantity": "- 米", + "notes": "量未指定" + }, + { + "name": "粟米(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 粟米(可选)", + "notes": "量未指定" + }, + { + "name": "金菇(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 金菇(可选)", + "notes": "量未指定" + }, + { + "name": "冬菇(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 冬菇(可选)", + "notes": "量未指定" + }, + { + "name": "米", + "quantity": null, + "unit": null, + "text_quantity": "- 米 1 杯 / 人", + "notes": "量未指定" + }, + { + "name": "三文鱼", + "quantity": null, + "unit": null, + "text_quantity": "- 三文鱼 300g / 人", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "牛油一汤匙 / 人", + "quantity": null, + "unit": null, + "text_quantity": "- 牛油一汤匙 / 人", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "三文鱼去鳞,去骨" + }, + { + "step": 2, + "description": "金菇、冬菇切碎" + }, + { + "step": 3, + "description": "洗米三次" + }, + { + "step": 4, + "description": "把三文鱼、米、牛油放入电饭煲" + }, + { + "step": 5, + "description": "想口感浓厚一点,可以加多一汤匙牛油" + }, + { + "step": 6, + "description": "根据电饭煲的刻度放水" + }, + { + "step": 7, + "description": "把电饭煲調較至煲飯模式,等待大約 30 - 45 分鐘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-空气炸锅照烧鸡饭-空气炸锅照烧鸡饭", + "name": "空气炸锅照烧鸡饭的做法", + "description": "# 空气炸锅照烧鸡饭的做法\n\n![空气炸锅照烧鸡饭成品](./空气炸锅照烧鸡饭.jpg)\n\n空气炸锅照烧鸡饭是一道简单易做的菜。是一道既便利又便宜的美食,而且在品尝美味的同时,新手也能完全掌握!\n\n预估烹饪难度:★★★★", + "source_path": "dishes/staple/空气炸锅照烧鸡饭/空气炸锅照烧鸡饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/空气炸锅照烧鸡饭/空气炸锅照烧鸡饭.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/空气炸锅照烧鸡饭/空气炸锅照烧鸡饭.jpg" + ], + "category": "主食", + "difficulty": 4, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "丽滋饼干(Ritz crackers)", + "quantity": null, + "unit": null, + "text_quantity": "- 丽滋饼干(Ritz crackers)", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "糖(白沙糖)", + "quantity": null, + "unit": null, + "text_quantity": "- 糖(白沙糖)", + "notes": "量未指定" + }, + { + "name": "鸡肉", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡肉 900g", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 100-125ml", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 60-65g", + "notes": "量未指定" + }, + { + "name": "白醋", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋 30-35ml", + "notes": "量未指定" + }, + { + "name": "丽滋饼干(咸味曲奇可替代)", + "quantity": null, + "unit": null, + "text_quantity": "- 丽滋饼干(咸味曲奇可替代) 16 个(48g)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将酱油、糖和醋混合在一起,搅匀料汁备用" + }, + { + "step": 2, + "description": "另一个碗中加入鸡肉、鸡蛋、1/2 料汁和压碎的丽滋饼干。搅拌均匀" + }, + { + "step": 3, + "description": "空气炸锅用箔纸碗铺底,加入肉饼混合物,将剩余的料汁均匀的倒在上面" + }, + { + "step": 4, + "description": "**350°** 炸**40 分钟**。最好在米饭上食用" + }, + { + "step": 5, + "description": "在外观*呈金黄酥脆*后出锅,切块盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-米饭-煮锅蒸米饭", + "name": "煮锅蒸米饭的做法", + "description": "# 煮锅蒸米饭的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/米饭/煮锅蒸米饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg" + ], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "北方大米", + "quantity": null, + "unit": null, + "text_quantity": "- 北方大米", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "厚底煮锅+严丝合缝的锅盖(制作过程中不会有大量蒸汽泄漏)", + "quantity": null, + "unit": null, + "text_quantity": "- 厚底煮锅+严丝合缝的锅盖(制作过程中不会有大量蒸汽泄漏)", + "notes": "量未指定" + }, + { + "name": "米:100ml-200ml/人", + "quantity": null, + "unit": null, + "text_quantity": "- 米:100ml-200ml/人", + "notes": "量未指定" + }, + { + "name": "水:米的体积的", + "quantity": null, + "unit": null, + "text_quantity": "- 水:米的体积的 2 倍", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "清洗大米" + }, + { + "step": 2, + "description": "将米和水加入煮锅" + }, + { + "step": 3, + "description": "大火煮至水沸腾" + }, + { + "step": 4, + "description": "**搅拌底部防止粘黏**" + }, + { + "step": 5, + "description": "盖上锅盖,转**小火**加热 10-15 分钟(根据对软糯程度的喜好),中途切勿打开锅盖" + }, + { + "step": 6, + "description": "关火,静置 5 分钟" + }, + { + "step": 7, + "description": "Enjoy :)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-米饭-电饭煲蒸米饭", + "name": "电饭煲蒸米饭的做法", + "description": "# 电饭煲蒸米饭的做法\n\n预估烹饪难度:★", + "source_path": "dishes/staple/米饭/电饭煲蒸米饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg" + ], + "category": "主食", + "difficulty": 1, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "电饭煲", + "quantity": null, + "unit": null, + "text_quantity": "- 电饭煲", + "notes": "量未指定" + }, + { + "name": "江南米或北方大米", + "quantity": null, + "unit": null, + "text_quantity": "- 江南米或北方大米", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "一般一个人可以食用", + "quantity": null, + "unit": null, + "text_quantity": "- 一般一个人可以食用 100ml-200ml 的米。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "清洗米" + }, + { + "step": 2, + "description": "将米和水一起加入电饭煲中。" + }, + { + "step": 3, + "description": "连接电饭煲电源,进入加热模式。等待大约 30 分钟。" + }, + { + "step": 4, + "description": "待电饭煲自动进入保温模式后。" + }, + { + "step": 5, + "description": "将米在电饭煲中闷 10-15 分钟。" + }, + { + "step": 6, + "description": "盛出米。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-老友猪肉粉-老友猪肉粉", + "name": "老友猪肉粉的做法", + "description": "# 老友猪肉粉的做法\n\n![示例菜成品](老友猪肉粉.jpg)\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/老友猪肉粉/老友猪肉粉.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/老友猪肉粉/老友猪肉粉.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/老友猪肉粉/老友猪肉粉.jpg" + ], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 米粉", + "notes": "量未指定" + }, + { + "name": "猪肉", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉", + "notes": "量未指定" + }, + { + "name": "酸笋", + "quantity": null, + "unit": null, + "text_quantity": "- 酸笋", + "notes": "量未指定" + }, + { + "name": "剁椒", + "quantity": null, + "unit": null, + "text_quantity": "- 剁椒", + "notes": "量未指定" + }, + { + "name": "豆豉", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "米醋", + "quantity": null, + "unit": null, + "text_quantity": "- 米醋", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "生粉", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉", + "notes": "量未指定" + }, + { + "name": "胡椒粉", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉", + "notes": "量未指定" + }, + { + "name": "米粉(250g 记得", + "quantity": null, + "unit": null, + "text_quantity": "- 米粉(250g 记得 50 度的温水泡半小时)", + "notes": "量未指定" + }, + { + "name": "猪肉(50g)", + "quantity": null, + "unit": null, + "text_quantity": "- 猪肉(50g)", + "notes": "量未指定" + }, + { + "name": "酸笋(50g)", + "quantity": null, + "unit": null, + "text_quantity": "- 酸笋(50g)", + "notes": "量未指定" + }, + { + "name": "剁椒(15g 辣椒剁完后, 个人需求适当放。 )", + "quantity": null, + "unit": null, + "text_quantity": "- 剁椒(15g 辣椒剁完后, 个人需求适当放。 )", + "notes": "量未指定" + }, + { + "name": "豆豉(30g)", + "quantity": null, + "unit": null, + "text_quantity": "- 豆豉(30g)", + "notes": "量未指定" + }, + { + "name": "大蒜(10g)", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜(10g)", + "notes": "量未指定" + }, + { + "name": "料酒(10-20ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒(10-20ml)", + "notes": "量未指定" + }, + { + "name": "生抽(15ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽(15ml)", + "notes": "量未指定" + }, + { + "name": "白糖(5g 如果不喜欢糖,可以考虑不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖(5g 如果不喜欢糖,可以考虑不放)", + "notes": "量未指定" + }, + { + "name": "米醋(5ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 米醋(5ml)", + "notes": "量未指定" + }, + { + "name": "盐(5ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 盐(5ml)", + "notes": "量未指定" + }, + { + "name": "油(15ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 油(15ml)", + "notes": "量未指定" + }, + { + "name": "生粉(15ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 生粉(15ml)", + "notes": "量未指定" + }, + { + "name": "胡椒粉(10ml)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡椒粉(10ml)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "全部猪肉用料酒、盐、生抽、生粉、胡椒粉倒在一个碗里调味,备用" + }, + { + "step": 2, + "description": "热锅不放油,下全部酸笋把水份炒干,炒干的酸笋中间留点空间" + }, + { + "step": 3, + "description": "放入 10ml - 15ml 食用油与全部大蒜、 剁椒、 豆豉到炒干的酸笋中间到炒干的酸笋中间,全部推到中间炒出香味" + }, + { + "step": 4, + "description": "放入全部调味好的猪肉,持续放入 10ml 生抽炒一分钟" + }, + { + "step": 5, + "description": "放入 5ml 米醋、 10ml 生抽、450ml 清水一起煮开" + }, + { + "step": 6, + "description": "水煮开后,放入温水泡好的米粉,继续煮 3 分钟就可以盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-茄子肉煎饼-茄子肉煎饼", + "name": "茄子肉煎饼的做法", + "description": "# 茄子肉煎饼的做法\n\n![茄子肉煎饼成品](./茄子肉煎饼.jpg)\n\n茄子肉煎饼是一道简单易做的饼类主食。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/茄子肉煎饼/茄子肉煎饼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/1茄片肉片.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/1茄片肉片.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/2米粉250g.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/3米粉面粉鸡蛋.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/4混合.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/5起锅烧油.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/6开始煎.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/7撒盐准备起锅.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/茄子肉煎饼.jpg" + ], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "米粉(指用大米研磨成的粉)", + "quantity": null, + "unit": null, + "text_quantity": "- 米粉(指用大米研磨成的粉)", + "notes": "量未指定" + }, + { + "name": "小麦粉", + "quantity": null, + "unit": null, + "text_quantity": "- 小麦粉", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "煮熟的腊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 煮熟的腊肉", + "notes": "量未指定" + }, + { + "name": "茄子(买长条状的,越圆越好)", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子(买长条状的,越圆越好)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐", + "notes": "量未指定" + }, + { + "name": "米粉", + "quantity": null, + "unit": null, + "text_quantity": "- 米粉 250g", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 50g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "煮熟的腊肉", + "quantity": null, + "unit": null, + "text_quantity": "- 煮熟的腊肉 100g", + "notes": "量未指定" + }, + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子 1 根(约 10-15cm 长)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 1-2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将茄子去皮后切成片,将腊肉切成片,备用" + }, + { + "step": 2, + "description": "依次向盆中加入 250g 米粉(大米研磨成的粉)、50g 面粉和 1 个鸡蛋" + }, + { + "step": 3, + "description": "边用筷子搅拌,边加入清水(**清水用于调节粘稠度**),使米粉、面粉、鸡蛋混合成面糊,当面糊能够附着在茄片、肉片上而不掉落时停止加水,而后将所有茄片和肉片放入面糊中,用面糊充分包裹" + }, + { + "step": 4, + "description": "平底锅加入食用油**10-30ml**,开小火" + }, + { + "step": 5, + "description": "用筷子或勺子把裹了面糊的茄片、肉片放入锅中,先煎至两面金黄,再煎**3-6分钟**(**煎的过程中,食用油会变少,可再添加食用油**)" + }, + { + "step": 6, + "description": "撒盐,翻炒均匀,起锅装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-西红柿鸡蛋挂面-西红柿鸡蛋挂面", + "name": "西红柿鸡蛋挂面的做法", + "description": "# 西红柿鸡蛋挂面的做法\n\n挂面太多怎么办?只煮个白水面味道难以下咽怎么办?简单的食材煮个美味的面条怎么操作?\n西红柿鸡蛋挂面只需简单的食材,快速的操作,不多的厨具,解决**不想麻烦**、**挂面太多**、**食材简单**的所有烦恼\n此处更要鸣谢 my mother 的在线指导:v:\n简单好做,开始吧!\n制作时间:20 分钟\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/西红柿鸡蛋挂面/西红柿鸡蛋挂面.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/food.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/food.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/fryEgg.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/pretreatFood.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/tomato.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/tomatoNoodle.jpg" + ], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "挂面或者鲜面条也行", + "quantity": null, + "unit": null, + "text_quantity": "- 挂面或者鲜面条也行", + "notes": "量未指定" + }, + { + "name": "西红柿一个", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿一个", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "酱油、蚝油或者鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油、蚝油或者鸡精", + "notes": "量未指定" + }, + { + "name": "白砂糖(中和西红柿的酸味,西红柿如果不酸就不用加)", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖(中和西红柿的酸味,西红柿如果不酸就不用加)", + "notes": "量未指定" + }, + { + "name": "青椒(非线椒)", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒(非线椒)", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "挂面", + "quantity": null, + "unit": null, + "text_quantity": "- 挂面 1 把(根据食量来)50-100g", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 1 个大概 200g 吧。", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1~2 个", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5g 或鸡精 3g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 2g", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 5-8g", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "小葱洗净并切成葱花" + }, + { + "step": 2, + "description": "西红柿切块儿,如果不太会切建议先百度一下~" + }, + { + "step": 3, + "description": "青椒切成菱形块" + }, + { + "step": 4, + "description": "生鸡蛋打入一个小碗并打散,如果鸡蛋有点腥味可以加 2g 白醋去腥" + }, + { + "step": 5, + "description": "起锅烧热,倒入 15~20g 食用油,鸡蛋炒嫩滑就得多一点油,同时为后面煸炒西红柿留一些底油" + }, + { + "step": 6, + "description": "待油温到七成热时(手掌隔大概 10cm,能感觉到热),倒入蛋液快速划散" + }, + { + "step": 7, + "description": "鸡蛋滑到凝固后,一点不会有蛋液了后倒入小碗备用,此处留一些底油" + }, + { + "step": 8, + "description": "锅中留底油后先加入葱白、蒜末炒香" + }, + { + "step": 9, + "description": "加入西红柿块、青椒,待西红柿炒出一点汁水" + }, + { + "step": 10, + "description": "此时速速加入 5g 酱油和 2g 白砂糖" + }, + { + "step": 11, + "description": "翻炒十几秒后加入一碗清水(刚刚好即将没过西红柿即可)" + }, + { + "step": 12, + "description": "煮沸后加入炒好的鸡蛋,加入蚝油 5g 或者 2g 鸡精用于提鲜" + }, + { + "step": 13, + "description": "中小火收汁,期间要搅拌防止粘锅,收汁到下图后加一点葱花(剩下的葱绿部分)和香油(不加也可以),臊子制作完成" + }, + { + "step": 14, + "description": "可以不用洗锅,直接加清水 500ml" + }, + { + "step": 15, + "description": "煮沸加入挂面,挂面煮软后加入 100ml 清水" + }, + { + "step": 16, + "description": "再次煮沸后,若面条飘起来了,再加入 100ml 清水" + }, + { + "step": 17, + "description": "煮沸后看面条两侧是否呈透明状,透明状则熟了" + }, + { + "step": 18, + "description": "捞面到臊子碗中,拌面即可啦~" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-豆角焖面-豆角焖面", + "name": "豆角焖面的做法", + "description": "# 豆角焖面的做法\n\n豆角焖面是一道懒人美食,操作简单,方便美味。\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/豆角焖面/豆角焖面.md", + "image_path": null, + "images": [], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "鲜面条(韭叶 or 二细<解释见最下方 关于面条粗细区别一栏)", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜面条(韭叶 or 二细<解释见最下方 关于面条粗细区别一栏)", + "notes": "量未指定" + }, + { + "name": "肉(最好为五花肉)", + "quantity": null, + "unit": null, + "text_quantity": "- 肉(最好为五花肉)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "豆角", + "quantity": null, + "unit": null, + "text_quantity": "- 豆角", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "耗油", + "quantity": null, + "unit": null, + "text_quantity": "- 耗油", + "notes": "量未指定" + }, + { + "name": "味精", + "quantity": null, + "unit": null, + "text_quantity": "- 味精", + "notes": "量未指定" + }, + { + "name": "十三香", + "quantity": null, + "unit": null, + "text_quantity": "- 十三香", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "热水", + "quantity": null, + "unit": null, + "text_quantity": "- 热水", + "notes": "量未指定" + }, + { + "name": "菜刀", + "quantity": null, + "unit": null, + "text_quantity": "- 菜刀", + "notes": "量未指定" + }, + { + "name": "鲜面条", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜面条 300g。", + "notes": "量未指定" + }, + { + "name": "肉", + "quantity": null, + "unit": null, + "text_quantity": "- 肉 100g。", + "notes": "量未指定" + }, + { + "name": "豆角", + "quantity": null, + "unit": null, + "text_quantity": "- 豆角 150g。", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 10g。", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 5g。", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 10g。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将豆角切成 5cm - 6cm 的小段。" + }, + { + "step": 2, + "description": "将葱切成 1cm - 2cm 小段。" + }, + { + "step": 3, + "description": "将姜切成 1mm x 1mm x 3cm 的长条" + }, + { + "step": 4, + "description": "将蒜放在砧板上拍碎,切成 1mm 的粒度。" + }, + { + "step": 5, + "description": "将五花肉切成 2mm 厚度的肉片。" + }, + { + "step": 6, + "description": "首先将锅烧热,烧去锅内全部水汽,手放过内距离锅底 10cm 处,感觉明显有些许烤手。" + }, + { + "step": 7, + "description": "加入上述定量的食用油,手持锅柄,离灶 5cm 处,摇晃锅,使食用油充分挂满锅的三分之二(自下而上)。" + }, + { + "step": 8, + "description": "放入全部的姜和全部的葱段,翻炒爆香 5 秒(注意!此时有油飞溅的危险,建议带上手套或做好防护措施)。" + }, + { + "step": 9, + "description": "放入全部的肉片,放入以后不着急饭锅,静置 5 秒后,再翻炒,使所有的肉都裹满食用油。" + }, + { + "step": 10, + "description": "不断翻炒肉片,待到全部肉片都已经变色,沿锅边均匀淋如准备好的生抽,翻炒均匀。" + }, + { + "step": 11, + "description": "依次加入准备好的盐、老抽、耗油、十三香、鸡精以及全部准备好的豆角,翻炒 2 分钟。" + }, + { + "step": 12, + "description": "加入准备好的热水。" + }, + { + "step": 13, + "description": "水开使用勺子舀出锅内 2 分之一菜汤(注意!不要将菜舀出)。" + }, + { + "step": 14, + "description": "将所有面条平铺在菜的上方。" + }, + { + "step": 15, + "description": "盖上锅盖,中火焖 5 分钟。" + }, + { + "step": 16, + "description": "打开锅盖,将舀出的菜汤使用勺子,以每次一勺的量,均匀撒在面条上。" + }, + { + "step": 17, + "description": "盖上锅盖,中火焖 3 分钟。" + }, + { + "step": 18, + "description": "打开锅盖,将所有的蒜、味精均匀撒入。" + }, + { + "step": 19, + "description": "使用筷子不断翻炒,将菜与肉均匀搅拌。" + }, + { + "step": 20, + "description": "关火" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-酱拌荞麦面-酱拌荞麦面", + "name": "酱拌荞麦面的做法", + "description": "# 酱拌荞麦面的做法\n\n酱拌荞麦面营养健康、酸甜可口\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/酱拌荞麦面/酱拌荞麦面.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/酱拌荞麦面/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/酱拌荞麦面/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/酱拌荞麦面/2.jpeg" + ], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "荞麦面", + "quantity": null, + "unit": null, + "text_quantity": "- 荞麦面", + "notes": "量未指定" + }, + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜", + "notes": "量未指定" + }, + { + "name": "红萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 红萝卜", + "notes": "量未指定" + }, + { + "name": "老干妈", + "quantity": null, + "unit": null, + "text_quantity": "- 老干妈", + "notes": "量未指定" + }, + { + "name": "荞麦面", + "quantity": null, + "unit": null, + "text_quantity": "- 荞麦面 100 g", + "notes": "量未指定" + }, + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜 0.5 根", + "notes": "量未指定" + }, + { + "name": "红萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 红萝卜 0.5 根", + "notes": "量未指定" + }, + { + "name": "老干妈", + "quantity": null, + "unit": null, + "text_quantity": "- 老干妈 20 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "荞麦面下冷水煮熟,8-10 分钟 后捞出沥干备用" + }, + { + "step": 2, + "description": "黄瓜、萝卜 切成小条" + }, + { + "step": 3, + "description": "将荞麦面、黄瓜、萝卜放入盘子,放上老干妈,搅拌" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-韩式拌饭-韩式拌饭", + "name": "韩式拌饭的做法", + "description": "# 韩式拌饭的做法\n\n![韩式拌饭](./韩式拌饭.png)\n\n预估烹饪难度:★★★", + "source_path": "dishes/staple/韩式拌饭/韩式拌饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/韩式拌饭/韩式拌饭.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/韩式拌饭/韩式拌饭.png" + ], + "category": "主食", + "difficulty": 3, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "火锅牛肉卷", + "quantity": null, + "unit": null, + "text_quantity": "- 火锅牛肉卷", + "notes": "量未指定" + }, + { + "name": "豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 豆芽", + "notes": "量未指定" + }, + { + "name": "蘑菇", + "quantity": null, + "unit": null, + "text_quantity": "- 蘑菇", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜", + "notes": "量未指定" + }, + { + "name": "西葫芦", + "quantity": null, + "unit": null, + "text_quantity": "- 西葫芦", + "notes": "量未指定" + }, + { + "name": "韩式辣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 韩式辣酱", + "notes": "量未指定" + }, + { + "name": "雪碧", + "quantity": null, + "unit": null, + "text_quantity": "- 雪碧", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻", + "notes": "量未指定" + }, + { + "name": "芝麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油", + "notes": "量未指定" + }, + { + "name": "米饭", + "quantity": null, + "unit": null, + "text_quantity": "- 米饭 1 碗 (400g)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 颗", + "notes": "量未指定" + }, + { + "name": "火锅牛肉卷", + "quantity": null, + "unit": null, + "text_quantity": "- 火锅牛肉卷 6 卷 60g", + "notes": "量未指定" + }, + { + "name": "豆芽", + "quantity": null, + "unit": null, + "text_quantity": "- 豆芽 1 把 80g", + "notes": "量未指定" + }, + { + "name": "蘑菇", + "quantity": null, + "unit": null, + "text_quantity": "- 蘑菇 50g", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 1/4 根", + "notes": "量未指定" + }, + { + "name": "西葫芦", + "quantity": null, + "unit": null, + "text_quantity": "- 西葫芦 50g", + "notes": "量未指定" + }, + { + "name": "韩式辣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 韩式辣酱 25ml", + "notes": "量未指定" + }, + { + "name": "雪碧", + "quantity": null, + "unit": null, + "text_quantity": "- 雪碧 2 瓶盖, 20ml", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻 10g", + "notes": "量未指定" + }, + { + "name": "芝麻油", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油 20ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "蔬菜清洗 切丝 放锅中翻炒 食材变软 便可称出" + }, + { + "step": 2, + "description": "煮水 等沸腾时 焯牛肉卷 只需煮熟 大概三分钟即可捞出" + }, + { + "step": 3, + "description": "煎[溏心蛋](../../breakfast/溏心蛋.md)" + }, + { + "step": 4, + "description": "将[米饭](../../staple/米饭/电饭煲蒸米饭.md)放在一个碗里 然后倒扣在大碗" + }, + { + "step": 5, + "description": "将准备好的蔬菜和肉卷依次绕圈放在米饭上面 将煎蛋放中间" + }, + { + "step": 6, + "description": "备酱汁" + }, + { + "step": 7, + "description": "将备好的酱汁倒在摆好盘的碗中" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-staple-鲣鱼海苔玉米饭-鲣鱼海苔玉米饭", + "name": "鲣鱼海苔玉米饭的做法", + "description": "# 鲣鱼海苔玉米饭的做法\n\n![示例菜成品](./米饭.jpg)\n\n空气炸锅羊排超级懒人版,味道尚可,主要看羊排的品质。\n\n- 烹饪总时长:40 分钟(准备 3 分钟+煮饭 40 分钟+拌饭 2 分钟)\n- 实际操作时间:5 分钟\n\n预估烹饪难度:★★", + "source_path": "dishes/staple/鲣鱼海苔玉米饭/鲣鱼海苔玉米饭.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/鲣鱼海苔玉米饭/米饭.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/鲣鱼海苔玉米饭/米饭.jpg" + ], + "category": "主食", + "difficulty": 2, + "tags": [ + "主食" + ], + "servings": 1, + "ingredients": [ + { + "name": "必备:鲣鱼海苔碎(JD 和淘宝都有,可以搜索:日式拌饭料)", + "quantity": null, + "unit": null, + "text_quantity": "- 必备:鲣鱼海苔碎(JD 和淘宝都有,可以搜索:日式拌饭料)", + "notes": "量未指定" + }, + { + "name": "必备:玉米粒(淘宝搜索:玉米粒 即食)", + "quantity": null, + "unit": null, + "text_quantity": "- 必备:玉米粒(淘宝搜索:玉米粒 即食)", + "notes": "量未指定" + }, + { + "name": "鲣鱼海苔碎", + "quantity": null, + "unit": null, + "text_quantity": "- 鲣鱼海苔碎 20g", + "notes": "量未指定" + }, + { + "name": "玉米粒", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒 80g/袋", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "盛好米饭,放入玉米粒拌好" + }, + { + "step": 2, + "description": "放入鲣鱼海苔碎" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-凉拌油麦菜", + "name": "凉拌油麦菜的做法", + "description": "# 凉拌油麦菜的做法\n\n预估烹饪难度:★", + "source_path": "dishes/vegetable_dish/凉拌油麦菜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 1, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "油麦菜", + "quantity": null, + "unit": null, + "text_quantity": "- 油麦菜", + "notes": "量未指定" + }, + { + "name": "芝麻酱", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻酱", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "1 颗 油麦菜(约", + "quantity": null, + "unit": null, + "text_quantity": "- 1 颗 油麦菜(约 200g) * 份数", + "notes": "量未指定" + }, + { + "name": "15ml 醋", + "quantity": null, + "unit": null, + "text_quantity": "- 15ml 醋 * 份数", + "notes": "量未指定" + }, + { + "name": "5ml 酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 5ml 酱油 * 份数", + "notes": "量未指定" + }, + { + "name": "10ml 芝麻酱", + "quantity": null, + "unit": null, + "text_quantity": "- 10ml 芝麻酱 * 份数", + "notes": "量未指定" + }, + { + "name": "5ml 香油", + "quantity": null, + "unit": null, + "text_quantity": "- 5ml 香油 * 份数", + "notes": "量未指定" + }, + { + "name": "5g 糖", + "quantity": null, + "unit": null, + "text_quantity": "- 5g 糖 * 份数", + "notes": "量未指定" + }, + { + "name": "10ml 蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 10ml 蚝油 * 份数", + "notes": "量未指定" + }, + { + "name": "两**头**蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 两**头**蒜 * 份数", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "蒜拍碎切末" + }, + { + "step": 2, + "description": "醋,酱油,芝麻酱,香油,糖,蚝油,蒜末放到碗里搅拌均匀" + }, + { + "step": 3, + "description": "油麦菜切段,每段不超过 4cm" + }, + { + "step": 4, + "description": "油麦菜放到一个大点的盆里,倒入上述碗中酱料,充分搅拌均匀." + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-凉拌豆腐", + "name": "凉拌豆腐的做法", + "description": "# 凉拌豆腐的做法\n\n凉拌豆腐是一道清爽可口的家常凉菜。富含植物蛋白和钙质,低脂健康,非常适合夏季食用或作为日常佐餐。制作过程简单快捷,一般初学者只需要 10 分钟即可完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/凉拌豆腐.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "豆腐 (推荐选用北豆腐或老豆腐)", + "quantity": null, + "unit": null, + "text_quantity": "- 豆腐 (推荐选用北豆腐或老豆腐)", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "醋(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 醋(可选)", + "notes": "量未指定" + }, + { + "name": "白糖(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖(可选)", + "notes": "量未指定" + }, + { + "name": "辣椒油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油(可选)", + "notes": "量未指定" + }, + { + "name": "豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 豆腐 250 g (约 1 块常见大小的豆腐)", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 10 g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 2-3 瓣", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15 ml", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 5 ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 5 ml(可选)", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 2 g(可选)", + "notes": "量未指定" + }, + { + "name": "辣椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油 5 ml(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 豆腐 切成 2 cm 见方的小块,备用。" + }, + { + "step": 2, + "description": "锅中加入 500 ml 饮用水,大火烧开。" + }, + { + "step": 3, + "description": "放入 豆腐 块,煮 **1-2 分钟**,以去除豆腥味并使豆腐口感更紧实。" + }, + { + "step": 4, + "description": "将 煮好的 豆腐 块捞出,沥干水分,放入碗中,备用。" + }, + { + "step": 5, + "description": "将 小葱 洗净,切成葱花,备用。" + }, + { + "step": 6, + "description": "将 大蒜 去皮,切成蒜末,备用。" + }, + { + "step": 7, + "description": "在一个干净的小碗中,加入 15 ml 生抽,5 ml 香油,5 ml 醋(可选),2 g 白糖(可选)。" + }, + { + "step": 8, + "description": "加入切好的 大蒜末。" + }, + { + "step": 9, + "description": "搅拌均匀,使 白糖 充分溶解,酱汁混合均匀。" + }, + { + "step": 10, + "description": "将制作好的酱汁均匀淋在 豆腐 块上。" + }, + { + "step": 11, + "description": "撒上切好的 小葱花。" + }, + { + "step": 12, + "description": "根据个人喜好,淋上 5 ml 辣椒油(可选)。" + }, + { + "step": 13, + "description": "用 筷子 或勺子轻轻拌匀,即可食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-凉拌金针菇", + "name": "凉拌金针菇的做法", + "description": "# 凉拌金针菇的做法\n\n凉拌金针菇是一道简单快捷的开胃凉菜。口感脆嫩爽滑,富含膳食纤维和多种维生素。制作过程无需复杂的烹饪技巧,非常适合新手和忙碌时快速准备。一般初学者只需要 10 分钟即可完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/凉拌金针菇.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "金针菇", + "quantity": null, + "unit": null, + "text_quantity": "- 金针菇", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "白糖(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖(可选)", + "notes": "量未指定" + }, + { + "name": "香油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 香油(可选)", + "notes": "量未指定" + }, + { + "name": "辣椒油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油(可选)", + "notes": "量未指定" + }, + { + "name": "金针菇", + "quantity": null, + "unit": null, + "text_quantity": "- 金针菇 150 g (约 1 小包)", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱 5 g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 2 瓣", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15 ml", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 10 ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 3 g(可选)", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 5 ml(可选)", + "notes": "量未指定" + }, + { + "name": "辣椒油", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒油 5 ml(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 金针菇 根部切除,用清水冲洗干净,备用。" + }, + { + "step": 2, + "description": "将 小葱 洗净,切成葱花,备用。" + }, + { + "step": 3, + "description": "将 大蒜 去皮,切成蒜末,备用。" + }, + { + "step": 4, + "description": "锅中加入 1000 ml 饮用水,大火烧开。" + }, + { + "step": 5, + "description": "放入 金针菇,煮 **1-2 分钟**,至金针菇变软。" + }, + { + "step": 6, + "description": "将 煮好的 金针菇 捞出,沥干水分,放入一个较大的碗中,备用。" + }, + { + "step": 7, + "description": "在另一个干净的小碗中,加入 15 ml 生抽,10 ml 醋,3 g 白糖(可选),5 ml 香油(可选)。" + }, + { + "step": 8, + "description": "加入切好的 大蒜末。" + }, + { + "step": 9, + "description": "搅拌均匀,使 白糖 充分溶解,酱汁混合均匀。" + }, + { + "step": 10, + "description": "将制作好的酱汁均匀淋在 金针菇 上。" + }, + { + "step": 11, + "description": "撒上切好的 小葱花。" + }, + { + "step": 12, + "description": "根据个人喜好,淋上 5 ml 辣椒油(可选)。" + }, + { + "step": 13, + "description": "用 筷子 轻轻拌匀,即可食用。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-凉拌黄瓜", + "name": "凉拌黄瓜的做法", + "description": "# 凉拌黄瓜的做法\n\n预估烹饪难度:★", + "source_path": "dishes/vegetable_dish/凉拌黄瓜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 1, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜 200 克 * 份数", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋 7.5 ml + 4 ml * 份数", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 5 ml + 2.5 ml * 份数", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 3 瓣 * 份数", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 0.4 克 + 0.2 克 * 份数", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 5 ml + 2 ml * 份数", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "用菜刀将黄瓜拍扁,再剁成长 3 厘米的碎块" + }, + { + "step": 2, + "description": "将碎黄瓜装入碗中" + }, + { + "step": 3, + "description": "将蒜拍碎切成碎末" + }, + { + "step": 4, + "description": "将醋,酱油,盐,蚝油和蒜依次倒入碗中搅拌均匀并腌制 15 分钟" + }, + { + "step": 5, + "description": "将香油倒入碗中并均匀搅拌" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-地三鲜", + "name": "地三鲜的做法", + "description": "# 地三鲜的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/地三鲜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "尖椒", + "quantity": null, + "unit": null, + "text_quantity": "- 尖椒", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "豆瓣酱", + "quantity": null, + "unit": null, + "text_quantity": "- 豆瓣酱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子 100g", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 150g", + "notes": "量未指定" + }, + { + "name": "尖椒", + "quantity": null, + "unit": null, + "text_quantity": "- 尖椒 3 - 4 个", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "土豆洗净、去皮。茄子、尖椒洗净。" + }, + { + "step": 2, + "description": "葱 3g 切 0.5cm 小段。蒜 10g 剁碎。姜 10g 切沫。" + }, + { + "step": 3, + "description": "茄子、土豆、尖椒均切成 15g 的小块。" + }, + { + "step": 4, + "description": "热锅,加入 25ml 油。" + }, + { + "step": 5, + "description": "加入土豆,煎炸大约 3 分钟,待其到大约 8 分熟,以显示金黄色为准。" + }, + { + "step": 6, + "description": "捞出土豆,留下油。" + }, + { + "step": 7, + "description": "加入茄子,煎炸大约 40 秒,待其到大约 7 分熟,以显示金黄色为准。" + }, + { + "step": 8, + "description": "如果锅内已经没有流动的油,可以考虑补充 15ml 油。" + }, + { + "step": 9, + "description": "放入葱 3g。姜 10g。" + }, + { + "step": 10, + "description": "放入豆瓣酱 20ml。" + }, + { + "step": 11, + "description": "放入生抽 10ml。" + }, + { + "step": 12, + "description": "放入盐 8g。" + }, + { + "step": 13, + "description": "放入糖 10g。" + }, + { + "step": 14, + "description": "放入之前处理的土豆。" + }, + { + "step": 15, + "description": "放入尖椒。" + }, + { + "step": 16, + "description": "翻炒 1 分钟。" + }, + { + "step": 17, + "description": "放入蒜 10g" + }, + { + "step": 18, + "description": "加入 200ml 水和 20g 淀粉。" + }, + { + "step": 19, + "description": "待水开后,汤减少一半时,关火盛盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-松仁玉米", + "name": "松仁玉米的做法", + "description": "# 松仁玉米的做法\n\n松仁玉米是一道色香味俱全的家常菜,口感甜嫩清爽,松仁香脆,老少皆宜。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/松仁玉米.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "玉米粒(建议使用甜玉米)", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒(建议使用甜玉米)", + "notes": "量未指定" + }, + { + "name": "熟松子仁", + "quantity": null, + "unit": null, + "text_quantity": "- 熟松子仁", + "notes": "量未指定" + }, + { + "name": "胡萝卜(可选,增加色彩)", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜(可选,增加色彩)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "玉米粒", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒 200 g(可用罐头甜玉米,也可自备煮熟)", + "notes": "量未指定" + }, + { + "name": "熟松子仁", + "quantity": null, + "unit": null, + "text_quantity": "- 熟松子仁 30 g", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 50 g(切小丁,可省略)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15 ml", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 10 g", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 1 g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 5 g", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 20 ml(用于调淀粉水)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "玉米粒和胡萝卜丁提前焯水 1 分钟,捞出沥干备用" + }, + { + "step": 2, + "description": "热锅凉油,放入胡萝卜丁略炒,再加入玉米粒翻炒" + }, + { + "step": 3, + "description": "加入白砂糖和盐,炒匀" + }, + { + "step": 4, + "description": "混合水与淀粉成水淀粉,倒入锅中快速翻炒使汤汁略稠" + }, + { + "step": 5, + "description": "加入熟松仁翻炒均匀" + }, + { + "step": 6, + "description": "出锅装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-水油焖蔬菜", + "name": "水油焖蔬菜的做法", + "description": "# 水油焖蔬菜的做法\n\n水油焖蔬菜中添加了油,这提升了口感,并且可提升脂溶性维生素的摄入。相比生吃蔬菜,好处更多。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/水油焖蔬菜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "蚝油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油(可选)", + "notes": "量未指定" + }, + { + "name": "叶菜类蔬菜", + "quantity": null, + "unit": null, + "text_quantity": "- 叶菜类蔬菜", + "notes": "量未指定" + }, + { + "name": "叶菜类蔬菜:300g ~", + "quantity": null, + "unit": null, + "text_quantity": "- 叶菜类蔬菜:300g ~ 500g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "洗净蔬菜" + }, + { + "step": 2, + "description": "锅中加入 150ml 水,并烧开。(水不需要能完全没过蔬菜)" + }, + { + "step": 3, + "description": "加入 3g 盐" + }, + { + "step": 4, + "description": "(可选)加入 3ml 蚝油" + }, + { + "step": 5, + "description": "加入 2ml 食用油" + }, + { + "step": 6, + "description": "下菜, 翻拌一下,然后盖上锅盖焖 1 分钟" + }, + { + "step": 7, + "description": "盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-油醋爆蛋", + "name": "油醋爆蛋的做法", + "description": "# 油醋爆蛋的做法\n\n油醋爆蛋是十分简单但是色香味一绝的一道菜,属于湘菜。制作十分简单,大约十分钟左右。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/油醋爆蛋.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "小葱", + "quantity": null, + "unit": null, + "text_quantity": "- 小葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "咖喱块", + "quantity": null, + "unit": null, + "text_quantity": "- 咖喱块 115g", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 2 个(每个土豆大约重 120g,共约 240g)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋不需打散,直接打入碗中备用" + }, + { + "step": 2, + "description": "香葱切 3cm 长小段即可" + }, + { + "step": 3, + "description": "蒜瓣和小米辣放入打蒜器,打成沫" + }, + { + "step": 4, + "description": "将香醋、生抽、蚝油、白糖、水加入小碗,搅拌均匀作为糖醋料汁" + }, + { + "step": 5, + "description": "油热倒入鸡蛋,等鸡蛋凝固之后铲成大块,倒入蒜沫、小米辣沫、倒入糖醋料汁" + }, + { + "step": 6, + "description": "大火收汁、快出锅时加入葱段即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-清炒花菜", + "name": "清炒花菜的做法", + "description": "# 清炒花菜的做法\n\n清炒花菜是一道常见的家常素菜。富含维生素 C 和膳食纤维,口感脆嫩。做法简单,是一道快速上手的炒菜。一般初学者只需要 15 分钟即可完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/清炒花菜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "花菜", + "quantity": null, + "unit": null, + "text_quantity": "- 花菜", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "花菜 约", + "quantity": null, + "unit": null, + "text_quantity": "- 花菜 约 300 g (约 1/2 中等大小的花菜)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 2-3 瓣", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15 ml", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 50 ml (用于炒制过程)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 花菜 洗净,用刀或手掰成小朵,粗茎部分可以切片,备用。" + }, + { + "step": 2, + "description": "将 大蒜 去皮,切成蒜片,备用。" + }, + { + "step": 3, + "description": "锅中加入 1000 ml 饮用水,大火烧开。" + }, + { + "step": 4, + "description": "放入 花菜 朵,煮 **2-3 分钟**,至花菜颜色变浅,口感稍微软化。" + }, + { + "step": 5, + "description": "将 煮好的 花菜 捞出,沥干水分,备用。" + }, + { + "step": 6, + "description": "热锅,加入 15 ml 食用油,大火烧热。" + }, + { + "step": 7, + "description": "放入 蒜片,快速煸炒出香味。" + }, + { + "step": 8, + "description": "放入 焯好水的 花菜 朵,转中大火,快速翻炒约 **2 分钟**,使花菜均匀受热。" + }, + { + "step": 9, + "description": "加入 3 g 盐,继续翻炒均匀。" + }, + { + "step": 10, + "description": "沿锅边淋入 50 ml 饮用水,盖上锅盖,焖 **1 分钟**,帮助花菜完全熟透入味。" + }, + { + "step": 11, + "description": "开盖,快速翻炒均匀,即可出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-清蒸南瓜", + "name": "清蒸南瓜的做法", + "description": "# 清蒸南瓜的做法\n\n清蒸南瓜是一道制作极其简单的家常甜点或主食。它最大程度地保留了南瓜本身的天然甜味和营养,口感软糯。是健康饮食的不错选择。一般初学者只需要 15-20 分钟即可完成(主要为蒸的时间)。\n\n预估烹饪难度:★", + "source_path": "dishes/vegetable_dish/清蒸南瓜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 1, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "南瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 南瓜", + "notes": "量未指定" + }, + { + "name": "蒸锅", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸锅", + "notes": "量未指定" + }, + { + "name": "南瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 南瓜 300 g", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 1000 ml (用于蒸锅)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 南瓜 外皮洗净,去除瓜瓤和籽。" + }, + { + "step": 2, + "description": "将 南瓜 切成厚度大约 2 cm 的片,备用。" + }, + { + "step": 3, + "description": "在 蒸锅 的锅中加入 1000 ml 饮用水。" + }, + { + "step": 4, + "description": "将切好的 南瓜 片均匀摆放在盘中。" + }, + { + "step": 5, + "description": "待蒸锅中的水烧开后,将装有 南瓜 的盘子放入蒸锅中。" + }, + { + "step": 6, + "description": "盖上锅盖,保持大火蒸 **15-20 分钟**,直至南瓜变软,可以用筷子轻松穿透。" + }, + { + "step": 7, + "description": "关火,小心取出盘子。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-炒茄子", + "name": "炒茄子的做法", + "description": "# 炒茄子的做法\n\n家常炒茄子,简单易学,原料不复杂,其中可选项有无皆可。(但是八角强烈推荐)\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/炒茄子.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子", + "notes": "量未指定" + }, + { + "name": "八角(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 八角(可选)", + "notes": "量未指定" + }, + { + "name": "虾皮(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 虾皮(可选)", + "notes": "量未指定" + }, + { + "name": "香葱(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱(可选)", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "菜籽油或花生油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油或花生油", + "notes": "量未指定" + }, + { + "name": "茄子数量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子数量 = 份数 * 1.8 个", + "notes": "量未指定" + }, + { + "name": "八角 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 八角 = 份数 * 1 个", + "notes": "量未指定" + }, + { + "name": "虾皮 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 虾皮 = 份数 * 正常男子手抓半把", + "notes": "量未指定" + }, + { + "name": "香葱 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 = 份数 * 2 颗", + "notes": "量未指定" + }, + { + "name": "酱油 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 = 份数 * 40 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将茄子洗净,一刀分为两段(竖切)。每段的茄子切菱形块,将切好的茄子放入碗中待命。" + }, + { + "step": 2, + "description": "将香葱洗净,并切成葱花放到案板上待命。" + }, + { + "step": 3, + "description": "切好八角,放到案板上待命。" + }, + { + "step": 4, + "description": "开火热锅,直至锅内没有水。" + }, + { + "step": 5, + "description": "往锅内倒食用油,没过锅底的两倍(油可以多加,但不可少加)。" + }, + { + "step": 6, + "description": "热油约 6 成熟,放入八角、虾皮、香葱这三种可选性材料。" + }, + { + "step": 7, + "description": "如果没有八角等可选材料,热油至 9 成熟。" + }, + { + "step": 8, + "description": "待锅内的油到 9 成熟,将碗中的茄子倒入锅内用锅铲进行翻炒。" + }, + { + "step": 9, + "description": "翻炒约 40 秒,将锅铲悬空,与锅平行,把酱油倒入锅铲内。一人约 2.5 锅铲(酱油可以少加,但不可多加,会咸)" + }, + { + "step": 10, + "description": "继续进行翻炒。" + }, + { + "step": 11, + "description": "等到锅内所有茄子变色且变软时捞出。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-炒青菜", + "name": "炒青菜的做法", + "description": "# 炒青菜的做法\n\n制作简单方便。预计 10 分钟即可完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/炒青菜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "青菜", + "quantity": null, + "unit": null, + "text_quantity": "- 青菜", + "notes": "量未指定" + }, + { + "name": "青菜", + "quantity": null, + "unit": null, + "text_quantity": "- 青菜 100g * 份数", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml(覆盖锅底即可)", + "notes": "量未指定" + }, + { + "name": "食盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食盐 2g * 份数", + "notes": "量未指定" + }, + { + "name": "饮用水", + "quantity": null, + "unit": null, + "text_quantity": "- 饮用水 70ml * 份数", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 5g * 份数", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "青菜掰成小瓣,用清水洗净,备用。" + }, + { + "step": 2, + "description": "中火或大火热锅后,锅内放入 10-15ml 食用油。再等待 30 秒让油温升高。" + }, + { + "step": 3, + "description": "将准备好的青菜倒入锅中,翻炒至青菜变软(约 1 分钟)。" + }, + { + "step": 4, + "description": "倒入计算好的清水,水位应当完全浸润或即将没过青菜,加入食盐 (2g * 份数),继续翻炒约 1 分钟。" + }, + { + "step": 5, + "description": "最后加入白糖小火加热 2 分钟,加热时盖上锅盖。" + }, + { + "step": 6, + "description": "盛盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-素炒豆角", + "name": "素炒豆角的做法", + "description": "# 素炒豆角的做法\n\n巨下饭的家常菜\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/素炒豆角.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "豆角", + "quantity": null, + "unit": null, + "text_quantity": "- 豆角", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "耗油", + "quantity": null, + "unit": null, + "text_quantity": "- 耗油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "豆角", + "quantity": null, + "unit": null, + "text_quantity": "- 豆角 250g", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 2 个", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 3 圈", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2 颗", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 6ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 2ml", + "notes": "量未指定" + }, + { + "name": "耗油", + "quantity": null, + "unit": null, + "text_quantity": "- 耗油 6ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 6g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "葱切花,蒜切沫,备用。" + }, + { + "step": 2, + "description": "生抽、老抽、耗油、盐混合调料汁,备用。" + }, + { + "step": 3, + "description": "小米椒切圈,备用。" + }, + { + "step": 4, + "description": "豆角去筋,45° 斜切*4-10cm*小段,备用。" + }, + { + "step": 5, + "description": "起锅烧油(10ml - 15ml),冒烟后放入葱、小米椒,翻炒至闻到香味;" + }, + { + "step": 6, + "description": "加入豆角,翻炒*30s*," + }, + { + "step": 7, + "description": "加入料汁,开大火翻炒*2分钟*" + }, + { + "step": 8, + "description": "倒入 150ml 水" + }, + { + "step": 9, + "description": "转中小火,盖上锅盖焖制 8-10 分钟" + }, + { + "step": 10, + "description": "加入蒜切沫,出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-红烧茄子", + "name": "红烧茄子的做法", + "description": "# 红烧茄子的做法\n\n预估烹饪难度:★★★★", + "source_path": "dishes/vegetable_dish/红烧茄子.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 4, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "青辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青辣椒", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "青茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 青茄子", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "面粉", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "青茄子的数量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 青茄子的数量 = 份数 * 0.7 个", + "notes": "量未指定" + }, + { + "name": "青辣椒 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 青辣椒 = 份数 * 0.5 个", + "notes": "量未指定" + }, + { + "name": "洋葱 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 = 份数 * 0.3 个", + "notes": "量未指定" + }, + { + "name": "西红柿 =", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 = 1 个", + "notes": "量未指定" + }, + { + "name": "大葱 = 半颗", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 = 半颗", + "notes": "量未指定" + }, + { + "name": "大蒜 =", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 = 3 瓣", + "notes": "量未指定" + }, + { + "name": "鸡蛋 =", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 = 1 个", + "notes": "量未指定" + }, + { + "name": "面粉 = 青茄子数量", + "quantity": null, + "unit": null, + "text_quantity": "- 面粉 = 青茄子数量 * 150 克", + "notes": "量未指定" + }, + { + "name": "淀粉 = 面粉 /", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 = 面粉 / 4 克", + "notes": "量未指定" + }, + { + "name": "酱油 = 茄子数量", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 = 茄子数量 * 7 克(向上取整)", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-脆皮豆腐", + "name": "脆皮豆腐的做法", + "description": "# 脆皮豆腐的做法\n\n浓郁的酱汁裹满豆腐,吃一口就停不下来,别提有多好吃。\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/脆皮豆腐.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "老豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 老豆腐", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉", + "notes": "量未指定" + }, + { + "name": "平底锅", + "quantity": null, + "unit": null, + "text_quantity": "- 平底锅", + "notes": "量未指定" + }, + { + "name": "老豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 老豆腐 1 块 (市场买 1.25 格 * 人)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 只 * 老豆腐块数", + "notes": "量未指定" + }, + { + "name": "玉米淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米淀粉 50 g * 老豆腐块数", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 20 g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 10 g", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 5 g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 10 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋搅拌形成蛋液放置备用" + }, + { + "step": 2, + "description": "配置酱料 (20 g 生抽+10 g 蚝油+5 g 老抽+10 g 白糖+10 g 玉米淀粉+200 ml 清水)" + }, + { + "step": 3, + "description": "老豆腐切片 (个人建议,仅供参考 人 * 5 片,厚度 1.2 cm)" + }, + { + "step": 4, + "description": "玉米淀粉倒入盘中,将老豆腐片粘上淀粉后,粘上蛋液,放置一旁" + }, + { + "step": 5, + "description": "热锅,锅内放入 18ml 食用油。等待 10 秒让油温升高" + }, + { + "step": 6, + "description": "将粘上蛋液的老豆腐片均匀放入锅中,铺好锅底,小火煎至金黄翻面" + }, + { + "step": 7, + "description": "待两面煎至金黄后,倒入酱料,让每块豆腐都沐浴在酱料中,大火 3 分钟至酱汁浓稠" + }, + { + "step": 8, + "description": "关火" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-茄子炖土豆", + "name": "茄子炖土豆的做法", + "description": "# 茄子炖土豆的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/茄子炖土豆.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "肉", + "quantity": null, + "unit": null, + "text_quantity": "- 肉", + "notes": "量未指定" + }, + { + "name": "辣椒(是青辣椒,而**不是辣椒面或辣椒油**)", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒(是青辣椒,而**不是辣椒面或辣椒油**)", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "茄子的数量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子的数量 = 份数 * 1 个 (每个茄子约 150g)", + "notes": "量未指定" + }, + { + "name": "土豆数量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆数量 = 份数 * 1 个(每个土豆约 150g)", + "notes": "量未指定" + }, + { + "name": "肉量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 肉量 = 份数 * 180 克", + "notes": "量未指定" + }, + { + "name": "酱油量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油量 = 份数 * 15 毫升", + "notes": "量未指定" + }, + { + "name": "盐量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 份数 * 5 克", + "notes": "量未指定" + }, + { + "name": "辣椒量 =", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒量 = 50 克(调味,所以无论多少人都放这些。)", + "notes": "量未指定" + }, + { + "name": "蒜量 =", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜量 = 3 瓣(调味,所以无论多少人都放这些。注意是里面的小瓣 3 瓣,而**不是3整头**)", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-葱煎豆腐", + "name": "葱煎豆腐的做法", + "description": "# 葱煎豆腐的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/葱煎豆腐.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "白豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 白豆腐", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "青辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青辣椒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "平底锅", + "quantity": null, + "unit": null, + "text_quantity": "- 平底锅", + "notes": "量未指定" + }, + { + "name": "辣椒的数量 =", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒的数量 = 1.5 只/三人。", + "notes": "量未指定" + }, + { + "name": "葱的数量 =", + "quantity": null, + "unit": null, + "text_quantity": "- 葱的数量 = 2 根/三人。", + "notes": "量未指定" + }, + { + "name": "盐量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 盐量 = 份数 * 3g。", + "notes": "量未指定" + }, + { + "name": "鸡精量 = 份数", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精量 = 份数 * 1.5g。", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "豆腐洗净。切约 5 mm 厚度,置于碟中。" + }, + { + "step": 2, + "description": "葱洗净,除去根部,切成葱花,备用。" + }, + { + "step": 3, + "description": "辣椒洗净,切开,去籽,切成 1cm * 1cm 状,备用、" + }, + { + "step": 4, + "description": "热锅,加入份数 * 9ml 油。" + }, + { + "step": 5, + "description": "油入锅后,使其均匀布于锅底,均匀放入豆腐,小火煎至金黄翻面。" + }, + { + "step": 6, + "description": "待两面金黄,盛入碟中备用。" + }, + { + "step": 7, + "description": "补油至覆盖锅底,倒入辣椒大火翻炒,并铲碾 3 分钟。" + }, + { + "step": 8, + "description": "倒入豆腐,翻炒,加入盐与鸡精,中火翻炒 1 分钟后倒入 10 ML 水,大火收汁。" + }, + { + "step": 9, + "description": "出锅前撒上之前计算好的葱花,起锅盛盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-蒜蓉西兰花", + "name": "蒜蓉西兰花的做法", + "description": "# 蒜蓉西兰花的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/蒜蓉西兰花.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "西兰花", + "quantity": null, + "unit": null, + "text_quantity": "- 西兰花 1 个", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 3-4 瓣", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "西兰花 约", + "quantity": null, + "unit": null, + "text_quantity": "- 西兰花 约 200 g (约 1/2 中等大小的西兰花)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 3-4 瓣", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10 ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5 ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 2 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将 西兰花 切成小朵,清洗干净。" + }, + { + "step": 2, + "description": "将 大蒜 去皮,切成蒜末,备用。" + }, + { + "step": 3, + "description": "锅中加入 1000 ml 饮用水,大火烧开。" + }, + { + "step": 4, + "description": "放入 西兰花,保持大火 **煮 2-3 分钟**,至 西兰花 颜色变翠绿,口感变软。" + }, + { + "step": 5, + "description": "将 煮好的 西兰花 捞出,沥干水分,摆入盘中,备用。" + }, + { + "step": 6, + "description": "热锅,加入 10 ml 食用油。油温升高后,放入 大蒜末,小火煸炒出香味。" + }, + { + "step": 7, + "description": "加入 10 ml 生抽,5 ml 蚝油,2 g 白糖,加入 30 ml 饮用水。" + }, + { + "step": 8, + "description": "将锅中汤汁烧开。" + }, + { + "step": 9, + "description": "将烧好的蒜蓉汁 均匀淋在盘中的 西兰花 上。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-蒲烧茄子", + "name": "蒲烧茄子的做法", + "description": "# 蒲烧茄子的做法\n\n众所皆知,茄子🍆和土豆🥔是两种荤菜。这一道蒲烧茄子,从外观上之于鳗鱼正如`土豆炖.*`中的生姜之于土豆。\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/蒲烧茄子.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子", + "notes": "量未指定" + }, + { + "name": "蒲烧汁", + "quantity": null, + "unit": null, + "text_quantity": "- 蒲烧汁", + "notes": "量未指定" + }, + { + "name": "蜂蜜", + "quantity": null, + "unit": null, + "text_quantity": "- 蜂蜜", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "根据锅的类型决策不同量的油", + "quantity": null, + "unit": null, + "text_quantity": "- 根据锅的类型决策不同量的油", + "notes": "量未指定" + }, + { + "name": "不粘锅:油汇聚成滴后要散布在茄子的面积", + "quantity": null, + "unit": null, + "text_quantity": "- 不粘锅:油汇聚成滴后要散布在茄子的面积", + "notes": "量未指定" + }, + { + "name": "铁锅:摊开后油可以刚好覆盖锅底", + "quantity": null, + "unit": null, + "text_quantity": "- 铁锅:摊开后油可以刚好覆盖锅底", + "notes": "量未指定" + }, + { + "name": "1 个长的上小下大的茄子(注意不要使用浙茄和圆茄)", + "quantity": null, + "unit": null, + "text_quantity": "- 1 个长的上小下大的茄子(注意不要使用浙茄和圆茄)", + "notes": "量未指定" + }, + { + "name": "1 份蒲烧汁", + "quantity": null, + "unit": null, + "text_quantity": "- 1 份蒲烧汁", + "notes": "量未指定" + }, + { + "name": "20 ml 蜂蜜", + "quantity": null, + "unit": null, + "text_quantity": "- 20 ml 蜂蜜", + "notes": "量未指定" + }, + { + "name": "15 ml 白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 15 ml 白糖", + "notes": "量未指定" + }, + { + "name": "40 ml 生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 40 ml 生抽", + "notes": "量未指定" + }, + { + "name": "10 ml 老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 10 ml 老抽", + "notes": "量未指定" + }, + { + "name": "20 ml 料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 20 ml 料酒", + "notes": "量未指定" + }, + { + "name": "100 ml 水", + "quantity": null, + "unit": null, + "text_quantity": "- 100 ml 水", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "茄子削皮、横着切成两段" + }, + { + "step": 2, + "description": "蒸 5 分钟" + }, + { + "step": 3, + "description": "纵向切开,不要切断,在两边切面各划 2~3 刀至可以摊平" + }, + { + "step": 4, + "description": "煎至两面金黄" + }, + { + "step": 5, + "description": "往茄子上浇蒲烧汁至没过 1/2 茄子高度" + }, + { + "step": 6, + "description": "煎至背面上色,翻面" + }, + { + "step": 7, + "description": "把剩下的蒲烧汁浇在茄子上" + }, + { + "step": 8, + "description": "出锅,一份茄子烧蒲烧汁就烧好了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-蚝油生菜", + "name": "蚝油生菜的做法", + "description": "# 蚝油生菜的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/蚝油生菜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生菜", + "quantity": null, + "unit": null, + "text_quantity": "- 生菜 1 棵( 200 g ± 50 )", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 6-8 ml", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 4-5 瓣(做成蒜泥或切碎)", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 6 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 0.5 g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 1 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 5-8 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "生菜洗净并去掉烂菜叶。" + }, + { + "step": 2, + "description": "热锅,先放 1 L 清水(凉),然后在锅内放入 2 ml - 3 ml 食用油和 0.5 g 盐,等待锅中的水煮沸。" + }, + { + "step": 3, + "description": "水沸后,放入生菜,将**每一片**生菜叶都焯水 10 s。" + }, + { + "step": 4, + "description": "捞出生菜,控干水份,摆盘 。" + }, + { + "step": 5, + "description": "调汁:将生抽 10 ml 、蚝油 6-8 ml 、盐 0.5 g 、 白糖 1 g 放入碗中调匀,并加入 10-15 ml 清水(凉)搅拌均匀。" + }, + { + "step": 6, + "description": "再开火,热锅,放入食用油 5-8 ml,油热放入蒜泥。" + }, + { + "step": 7, + "description": "等待有蒜香飘出,倒入调好的汁,煮沸即可,立马关火。" + }, + { + "step": 8, + "description": "将锅中的汤汁均匀地**浇**在生菜上。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-西红柿炒鸡蛋", + "name": "西红柿炒鸡蛋的做法", + "description": "# 西红柿炒鸡蛋的做法\n\n西红柿炒蛋是中国家常几乎最常见的一道菜肴。它的原材料易于搜集,制作步骤也较为简单,所以非常适合新厨师上手,是很多人学习做菜时做的第一道菜。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/西红柿炒鸡蛋.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 糖(可选)", + "notes": "量未指定" + }, + { + "name": "葱花(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 葱花(可选)", + "notes": "量未指定" + }, + { + "name": "西红柿 =", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 = 1 个(约 180g) * 份数", + "notes": "量未指定" + }, + { + "name": "鸡蛋 =", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 = 1.5 个 * 份数,向上取整", + "notes": "量未指定" + }, + { + "name": "食用油 =", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 = 4ml * 鸡蛋/个", + "notes": "量未指定" + }, + { + "name": "盐 =", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 = 1.5-2g * 份数", + "notes": "量未指定" + }, + { + "name": "糖 =", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 = 0-2g * 份数", + "notes": "量未指定" + }, + { + "name": "葱花 =", + "quantity": null, + "unit": null, + "text_quantity": "- 葱花 = 0-10g * 份数", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "西红柿洗净" + }, + { + "step": 2, + "description": "可选:去掉西红柿的外表皮" + }, + { + "step": 3, + "description": "西红柿去蒂,切成边长不超过 4cm 的小块,即为 `西红柿块`" + }, + { + "step": 4, + "description": "将鸡蛋打入碗中,加入 `1g * 份数` 的盐,搅匀,即为 `鸡蛋液`" + }, + { + "step": 5, + "description": "热锅,加入食用油" + }, + { + "step": 6, + "description": "油热后,倒入 `鸡蛋液`。翻炒至鸡蛋结为固体且颜色微微发黄,即为 `半熟鸡蛋`" + }, + { + "step": 7, + "description": "关火。将 `半熟鸡蛋` 盛盘,重新开火" + }, + { + "step": 8, + "description": "加入 `西红柿块`,锅铲拍打并翻炒 20 秒,或至西红柿软烂" + }, + { + "step": 9, + "description": "向锅中加入 `半熟鸡蛋`,翻炒均匀" + }, + { + "step": 10, + "description": "加入剩余的盐、糖(可选,如果倾向于甜味版本)、葱花(可选),翻炒均匀" + }, + { + "step": 11, + "description": "关火,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-酸辣土豆丝", + "name": "酸辣土豆丝的做法", + "description": "# 酸辣土豆丝的做法\n\n酸辣土豆丝是一道简单易做的菜。色泽光亮,酸辣可口。辅料辣椒富含维生素 C。该菜用料简单,好学易做\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/酸辣土豆丝.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "红椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红椒", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "陈醋", + "quantity": null, + "unit": null, + "text_quantity": "- 陈醋", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 240g(越细越长更好)", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 4 瓣", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 0.5 个", + "notes": "量未指定" + }, + { + "name": "红椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红椒 0.5 个", + "notes": "量未指定" + }, + { + "name": "干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 干辣椒 3 个", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 1 根", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5ml", + "notes": "量未指定" + }, + { + "name": "陈醋", + "quantity": null, + "unit": null, + "text_quantity": "- 陈醋 10ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "土豆去皮、切丝(或用刨丝器)。" + }, + { + "step": 2, + "description": "切好的土豆丝用清水清洗,去除多余的淀粉,然后对土豆丝焯水 10 秒。沥干,备用。" + }, + { + "step": 3, + "description": "葱蒜干辣椒切小块,青红椒切丝。" + }, + { + "step": 4, + "description": "热锅,小火热油爆香蒜和干辣椒。" + }, + { + "step": 5, + "description": "加入青红椒翻炒几下,加入土豆丝翻炒至变色。" + }, + { + "step": 6, + "description": "加 5ml 生抽,10ml 陈醋,蒜末,最后加入盐翻炒均匀即可。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-金针菇日本豆腐煲", + "name": "金针菇日本豆腐煲的做法", + "description": "# 金针菇日本豆腐煲的做法\n\n金针菇日本豆腐煲是一道容易上手的日常料理。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/金针菇日本豆腐煲.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "金针菇", + "quantity": null, + "unit": null, + "text_quantity": "- 金针菇", + "notes": "量未指定" + }, + { + "name": "日本豆腐(玉子豆腐)", + "quantity": null, + "unit": null, + "text_quantity": "- 日本豆腐(玉子豆腐)", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "金针菇", + "quantity": null, + "unit": null, + "text_quantity": "- 金针菇 1-2 把", + "notes": "量未指定" + }, + { + "name": "豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 豆腐 2 袋", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 3-5 根,切碎", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2-3 瓣", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 3ml", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖 3g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "豆腐切片,小火煎到两面金黄出锅备用。" + }, + { + "step": 2, + "description": "切蒜成蒜末;将生抽,蚝油,老抽,糖,100ml 水调汁备用。" + }, + { + "step": 3, + "description": "热锅放油,油热放小米椒、蒜末爆香,先放金针菇,炒软,把煎好的豆腐平铺在金针菇上,倒入#2 配好的料汁,焖 5 分钟,大火收汁。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-陕北熬豆角", + "name": "陕北熬豆角的做法", + "description": "# 陕北熬豆角的做法\n\n陕北熬豆角是一种对初学者极其友善的菜,因其制作方式使用`熬`的方式,食材可多可少,可有可无,几乎不存在失败的可能性。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/陕北熬豆角.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "豆角", + "quantity": null, + "unit": null, + "text_quantity": "- 豆角", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "螺丝椒(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 螺丝椒(可选)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "豆角", + "quantity": null, + "unit": null, + "text_quantity": "- 豆角 300g * 2 人", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 1 个 * 2 人", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 1 个 * 2 人", + "notes": "量未指定" + }, + { + "name": "螺丝椒(可选)2 个", + "quantity": null, + "unit": null, + "text_quantity": "- 螺丝椒(可选)2 个 * 2 人", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 6g * 2 人", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 6ml * 2 人", + "notes": "量未指定" + }, + { + "name": "五香粉", + "quantity": null, + "unit": null, + "text_quantity": "- 五香粉 3g * 2 人", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 6ml * 2 人", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 3 圈", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 2g", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 2 颗", + "notes": "量未指定" + }, + { + "name": "香菜碎(可选)根据口味加", + "quantity": null, + "unit": null, + "text_quantity": "- 香菜碎(可选)根据口味加", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "葱切花,蒜切沫,姜切丝,备用。" + }, + { + "step": 2, + "description": "豆角去筋,切*2-10cm*小段,备用。" + }, + { + "step": 3, + "description": "土豆去皮,切*1cm³*小块,备用。" + }, + { + "step": 4, + "description": "西红柿去皮,切*1cm³*小块,备用。" + }, + { + "step": 5, + "description": "辣椒去仔,切*0.15cm 宽*条,备用。" + }, + { + "step": 6, + "description": "起锅烧油(10ml - 15ml),冒烟后放入葱姜蒜,翻炒至闻到葱姜蒜香味;" + }, + { + "step": 7, + "description": "加入豆角,翻炒至变色(青绿色变为翠绿色);" + }, + { + "step": 8, + "description": "加入土豆块,翻炒 30s;" + }, + { + "step": 9, + "description": "加入热水(水面刚刚漫过菜),盖上锅盖熬至土豆*变软*(可以用筷子确认);" + }, + { + "step": 10, + "description": "加入西红柿块,加入盐,生抽,蚝油,五香粉,辣椒,熬至西红柿成汁(注意搅拌,防止糊锅);" + }, + { + "step": 11, + "description": "加入香菜碎,出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-雷椒皮蛋", + "name": "雷椒皮蛋的做法", + "description": "# 雷椒皮蛋的做法\n\n雷椒皮蛋是一个非常简单的下饭凉菜,这道菜操作比较简单,且食材常见, 最终成品卖相不会很好看,但是是夏天下饭的神器之一\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/雷椒皮蛋.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "皮蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 皮蛋", + "notes": "量未指定" + }, + { + "name": "长条青椒(有些叫线椒,后面介绍以“青椒”代替)", + "quantity": null, + "unit": null, + "text_quantity": "- 长条青椒(有些叫线椒,后面介绍以“青椒”代替)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "小米辣(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣(可选)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "陈醋", + "quantity": null, + "unit": null, + "text_quantity": "- 陈醋", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "深一点的小铁盆", + "quantity": null, + "unit": null, + "text_quantity": "- 深一点的小铁盆", + "notes": "量未指定" + }, + { + "name": "皮蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 皮蛋 2 个", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 4 根 (长 10-15cm,宽 2-4cm)", + "notes": "量未指定" + }, + { + "name": "葱 (大约", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 (大约 10cm 即可,葱绿最佳)", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜 3-4 瓣", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-20ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15-20ml", + "notes": "量未指定" + }, + { + "name": "陈醋", + "quantity": null, + "unit": null, + "text_quantity": "- 陈醋 15-20ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 6-10g", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 5-7ml", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 3-4 颗", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "青椒清洗,去除根部,侧面切开,去除内部的子后在案板压平,备用(一定要去除青椒子,否则会在锅里炸开)" + }, + { + "step": 2, + "description": "葱切成半厘米小段,备用" + }, + { + "step": 3, + "description": "蒜去皮,切成碎末,备用" + }, + { + "step": 4, + "description": "皮蛋去皮,备用" + }, + { + "step": 5, + "description": "小米辣,切成 5-10mm 的小段,备用" + }, + { + "step": 6, + "description": "热锅,锅内放入 10ml - 20ml 食用油" + }, + { + "step": 7, + "description": "放入全部青椒,改小火保持锅子温度,煎至青椒变软(可以用筷子试一下,插入即透即可)" + }, + { + "step": 8, + "description": "关火,将皮蛋和青椒放入小铁盆中" + }, + { + "step": 9, + "description": "方法 1: 有擀面杖且砸东西不会吵到邻居:用擀面杖的一头在小盆中砸击皮蛋和青椒,至皮蛋与青椒混合(选项)" + }, + { + "step": 10, + "description": "方法 2:将青椒用手撕开,撕成大约半厘米的条状,用叉子将皮蛋压碎(选项)" + }, + { + "step": 11, + "description": "小米辣" + }, + { + "step": 12, + "description": "倒入生抽,陈醋,白糖,香油,以及其他未使用的备用食材" + }, + { + "step": 13, + "description": "均匀搅拌" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-鸡蛋火腿炒黄瓜", + "name": "鸡蛋火腿炒黄瓜的做法", + "description": "# 鸡蛋火腿炒黄瓜的做法\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/鸡蛋火腿炒黄瓜.md", + "image_path": null, + "images": [], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "火腿肠", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿肠", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "红尖椒(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 红尖椒(可选)", + "notes": "量未指定" + }, + { + "name": "黄瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 黄瓜 1 根(约 200g)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "火腿肠", + "quantity": null, + "unit": null, + "text_quantity": "- 火腿肠 1 根(约 40g)", + "notes": "量未指定" + }, + { + "name": "红尖椒", + "quantity": null, + "unit": null, + "text_quantity": "- 红尖椒 1 个(可选)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 3ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "黄瓜洗净,切半圆形片,备用" + }, + { + "step": 2, + "description": "火腿切半圆形片,备用" + }, + { + "step": 3, + "description": "红尖椒(可选)切碎,备用" + }, + { + "step": 4, + "description": "将鸡蛋打入碗中,搅匀,即为 `鸡蛋液`" + }, + { + "step": 5, + "description": "热锅里倒 5ml 食用油" + }, + { + "step": 6, + "description": "油热后转小火,倒入打散的`鸡蛋液`,用筷子划散,翻炒至鸡蛋结为固体且颜色微微发黄,即为 `半熟鸡蛋`,盛出备用" + }, + { + "step": 7, + "description": "**不用洗锅**,往锅内倒入 5ml 食用油,倒入黄瓜片大火**翻炒 1 分钟**" + }, + { + "step": 8, + "description": "把 `半熟鸡蛋` 倒入锅中,调入 2g 盐、3ml 生抽,立刻倒入火腿片和辣椒碎(可选)翻炒均匀" + }, + { + "step": 9, + "description": "关火,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-上汤娃娃菜-上汤娃娃菜", + "name": "上汤娃娃菜的做法", + "description": "# 上汤娃娃菜的做法\n\n上汤娃娃菜的做法 (素菜、减肥餐)\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/上汤娃娃菜/上汤娃娃菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/上汤娃娃菜/上汤娃娃菜.png", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/上汤娃娃菜/上汤娃娃菜.png" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "娃娃菜", + "quantity": null, + "unit": null, + "text_quantity": "- 娃娃菜", + "notes": "量未指定" + }, + { + "name": "皮蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 皮蛋", + "notes": "量未指定" + }, + { + "name": "午餐肉(火腿肠)", + "quantity": null, + "unit": null, + "text_quantity": "- 午餐肉(火腿肠)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "糖", + "quantity": null, + "unit": null, + "text_quantity": "- 糖", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "娃娃菜", + "quantity": null, + "unit": null, + "text_quantity": "- 娃娃菜 700g", + "notes": "量未指定" + }, + { + "name": "金针菇", + "quantity": null, + "unit": null, + "text_quantity": "- 金针菇 10g(看个人喜好, 不喜欢 see you tomorrow 的就不放 😂)", + "notes": "量未指定" + }, + { + "name": "皮蛋 一个(没有也可以不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 皮蛋 一个(没有也可以不放)", + "notes": "量未指定" + }, + { + "name": "午餐肉(火腿肠都可以替代)", + "quantity": null, + "unit": null, + "text_quantity": "- 午餐肉(火腿肠都可以替代)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "娃娃菜洗净, 竖着切开切成段。" + }, + { + "step": 2, + "description": "葱 3g 切 小段。蒜 10g 切片。姜 10g 切小片。" + }, + { + "step": 3, + "description": "皮蛋切成丁, 火腿肠或者午餐肉切成丁(1cm 大小的丁)" + }, + { + "step": 4, + "description": "金针菇洗净撕开" + }, + { + "step": 5, + "description": "烧热水娃娃菜放进去十秒钟出一下水捞出。" + }, + { + "step": 6, + "description": "热锅凉油, 加热锅倒入油过一遍就倒出来, 重新倒入一点油。" + }, + { + "step": 7, + "description": "调至小火加入葱姜蒜,煎炒出香味即可。" + }, + { + "step": 8, + "description": "加入适 300g 清水(水量没过娃娃菜即可), 放入娃娃菜, 金针菇, 午餐肉" + }, + { + "step": 9, + "description": "加入调味料蚝油、糖、盐、味精烧开。" + }, + { + "step": 10, + "description": "煮 3 分钟, 煮开后开始装盘, 盛出娃娃菜后皮蛋放在上面把汤汁浇上去就可以了" + }, + { + "step": 11, + "description": "![上汤娃娃菜](./上汤娃娃菜.png)" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-凉拌木耳-凉拌木耳", + "name": "凉拌木耳的做法", + "description": "# 凉拌木耳的做法\n\n凉拌木耳,由于发放物资中有很多干货,木耳是较为健康的食物。且凉拌木耳的烹饪方式也相对简单。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/凉拌木耳/凉拌木耳.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/1.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/10.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/2.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/3.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/4.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/5.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/6.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/7.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/8.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/9.jpg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "干木耳 (湿木耳也可,但不能太久之前泡发的,必须是新鲜的湿木耳)", + "quantity": null, + "unit": null, + "text_quantity": "- 干木耳 (湿木耳也可,但不能太久之前泡发的,必须是新鲜的湿木耳)", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "芥末 (可以不用)", + "quantity": null, + "unit": null, + "text_quantity": "- 芥末 (可以不用)", + "notes": "量未指定" + }, + { + "name": "干木耳:", + "quantity": null, + "unit": null, + "text_quantity": "- 干木耳: 20g / 湿木耳: 120g", + "notes": "量未指定" + }, + { + "name": "蒜瓣:", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣: 2-3 个", + "notes": "量未指定" + }, + { + "name": "小米辣:", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣: 2 个", + "notes": "量未指定" + }, + { + "name": "盐:", + "quantity": null, + "unit": null, + "text_quantity": "- 盐: 2 g", + "notes": "量未指定" + }, + { + "name": "糖:", + "quantity": null, + "unit": null, + "text_quantity": "- 糖: 5-10g(依个人口味)", + "notes": "量未指定" + }, + { + "name": "生抽:", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽: 15ml", + "notes": "量未指定" + }, + { + "name": "醋:", + "quantity": null, + "unit": null, + "text_quantity": "- 醋: 15ml", + "notes": "量未指定" + }, + { + "name": "香油:", + "quantity": null, + "unit": null, + "text_quantity": "- 香油: 5ml", + "notes": "量未指定" + }, + { + "name": "芥末: (约", + "quantity": null, + "unit": null, + "text_quantity": "- 芥末: (约 2cm)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "泡发干木耳, 水量约为 400ml, 泡发约 45 分钟。 (湿木耳跳过此步骤)" + }, + { + "step": 2, + "description": "将泡发好的木耳, 进行去根处理(如图 4, 5, 6), 并彻底洗净。" + }, + { + "step": 3, + "description": "起锅烧水,水开后放入木耳, 大火煮 1.5-2 分钟。" + }, + { + "step": 4, + "description": "将蒜瓣、小米辣切碎放入碗中 (可选取中大碗), 并依次加入盐、糖、生抽、醋、香油、芥末, 用量如上。" + }, + { + "step": 5, + "description": "木耳盛出后沥水, 放入上一步碗中。" + }, + { + "step": 6, + "description": "搅拌充分,端盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-凉拌莴笋-凉拌莴笋", + "name": "凉拌莴笋的做法", + "description": "# 凉拌莴笋的做法\n\n凉拌莴笋,开胃小菜\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/凉拌莴笋/凉拌莴笋.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌莴笋/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌莴笋/1.jpeg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "莴笋", + "quantity": null, + "unit": null, + "text_quantity": "- 莴笋", + "notes": "量未指定" + }, + { + "name": "萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 萝卜", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "莴笋", + "quantity": null, + "unit": null, + "text_quantity": "- 莴笋 1 根", + "notes": "量未指定" + }, + { + "name": "萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 萝卜 0.25 根", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 2 个", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 1 片", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 2 粒", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 25 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "莴笋削皮,切小条。萝卜切条,一起放入大碗,加入盐搅拌,放置 10 分钟" + }, + { + "step": 2, + "description": "放置后的莴笋用水清洗 1-2 遍" + }, + { + "step": 3, + "description": "起锅烧水,放入莴笋,水煮 1 分钟 捞出,沥干水分,放入大碗" + }, + { + "step": 4, + "description": "起锅烧油,放入姜片、蒜粒、小米椒煸炒 30-45 S ,倒入莴笋中" + }, + { + "step": 5, + "description": "搅拌充分,端盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-包菜炒鸡蛋粉丝-包菜炒鸡蛋粉丝", + "name": "包菜炒鸡蛋粉丝的做法", + "description": "# 包菜炒鸡蛋粉丝的做法\n\n包菜炒鸡蛋粉丝,是中国的一道日常生活中所熟知的菜品\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/包菜炒鸡蛋粉丝/包菜炒鸡蛋粉丝.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/包菜炒鸡蛋粉丝/1.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/包菜炒鸡蛋粉丝/1.jpg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "包菜", + "quantity": null, + "unit": null, + "text_quantity": "- 包菜", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "粉丝", + "quantity": null, + "unit": null, + "text_quantity": "- 粉丝", + "notes": "量未指定" + }, + { + "name": "胡萝卜", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油", + "notes": "量未指定" + }, + { + "name": "盐、生抽、老抽、蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 盐、生抽、老抽、蚝油", + "notes": "量未指定" + }, + { + "name": "葱、蒜、干辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 葱、蒜、干辣椒", + "notes": "量未指定" + }, + { + "name": "包菜 半 颗", + "quantity": null, + "unit": null, + "text_quantity": "- 包菜 半 颗", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "粉丝", + "quantity": null, + "unit": null, + "text_quantity": "- 粉丝 1 把", + "notes": "量未指定" + }, + { + "name": "胡萝卜 半 根", + "quantity": null, + "unit": null, + "text_quantity": "- 胡萝卜 半 根", + "notes": "量未指定" + }, + { + "name": "菜籽油", + "quantity": null, + "unit": null, + "text_quantity": "- 菜籽油 20 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 g、生抽 15 ml、老抽 10 ml、蚝油 10 ml", + "notes": "量未指定" + }, + { + "name": "葱 半 根、蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 半 根、蒜瓣 2 片、干辣椒 5 根", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "胡萝卜、包菜切丝备用" + }, + { + "step": 2, + "description": "粉丝先用冷水浸泡 1 小时,然后将粉丝放入锅中,加入开水烧至粉丝烫软捞出备用" + }, + { + "step": 3, + "description": "鸡蛋打入碗中,加入盐后搅拌 15 秒" + }, + { + "step": 4, + "description": "葱、蒜、辣椒切成小粒备用" + }, + { + "step": 5, + "description": "起锅烧油,倒入鸡蛋,打散炒熟盛出" + }, + { + "step": 6, + "description": "再倒入油,放入葱、蒜、干辣椒翻炒 8 秒" + }, + { + "step": 7, + "description": "下胡萝卜、包菜丝儿翻炒 30 秒" + }, + { + "step": 8, + "description": "放入粉丝" + }, + { + "step": 9, + "description": "放调料,生抽 15 ml,老抽 10 ml,蚝油 10 ml,盐 2 克" + }, + { + "step": 10, + "description": "放入之前炒好的鸡蛋,翻炒约 15 秒" + }, + { + "step": 11, + "description": "出锅摆盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-小炒藕丁-小炒藕丁", + "name": "小炒藕丁的做法", + "description": "# 小炒藕丁的做法\n\n![小炒藕丁成品](./小炒藕丁.jpg)\n\n小炒藕丁是一道简单易做的菜,莲藕营养丰富,非常适合素食。预计制作时长 20 分钟\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/小炒藕丁/小炒藕丁.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/小炒藕丁/小炒藕丁.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/小炒藕丁/小炒藕丁.jpg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "莲藕", + "quantity": null, + "unit": null, + "text_quantity": "- 莲藕", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "耗油", + "quantity": null, + "unit": null, + "text_quantity": "- 耗油", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "大葱", + "quantity": null, + "unit": null, + "text_quantity": "- 大葱 1 段", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 1-2 根 (看个人吃辣程度)", + "notes": "量未指定" + }, + { + "name": "莲藕", + "quantity": null, + "unit": null, + "text_quantity": "- 莲藕 1 段", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 30 ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 15 ml", + "notes": "量未指定" + }, + { + "name": "耗油", + "quantity": null, + "unit": null, + "text_quantity": "- 耗油 15 ml", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-15ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "大葱、小米辣切小段,备用" + }, + { + "step": 2, + "description": "莲藕去皮,切成不超过 3cm 的小块,放入水中备用(防止氧化发黑)" + }, + { + "step": 3, + "description": "取炒锅,锅内放入 500ml 凉水,煮沸" + }, + { + "step": 4, + "description": "将藕丁下入沸水中,焯水 2 分钟后,取出放入盘中备用" + }, + { + "step": 5, + "description": "将锅中水倒掉后,将锅加热干燥,加入 10-15 ml 食用油" + }, + { + "step": 6, + "description": "待油温升高后,下入葱花,小米辣爆香" + }, + { + "step": 7, + "description": "将处理好的藕丁下入锅中,大火翻炒" + }, + { + "step": 8, + "description": "加入生抽、老抽、耗油" + }, + { + "step": 9, + "description": "翻炒 2 分钟即可出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-干锅花菜-干锅花菜", + "name": "干锅花菜的做法", + "description": "# 干锅花菜的做法\n\n![干锅花菜成品](./干锅花菜.jpg)\n\n干锅花菜是湘菜常见的一道菜。\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/干锅花菜/干锅花菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/干锅花菜/干锅花菜.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/干锅花菜/干锅花菜.jpg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "花菜", + "quantity": null, + "unit": null, + "text_quantity": "- 花菜", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖", + "notes": "量未指定" + }, + { + "name": "蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "花菜", + "quantity": null, + "unit": null, + "text_quantity": "- 花菜 400 g", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 100 g", + "notes": "量未指定" + }, + { + "name": "辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒 1-2 根", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10 ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 5g", + "notes": "量未指定" + }, + { + "name": "蒜瓣", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜瓣 3-4 个", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 g", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 10 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "花菜朵朝下,没入淡盐水中浸泡 20 分钟。然后洗净用小刀拆成小朵" + }, + { + "step": 2, + "description": "入开水锅中焯水 1 分钟,捞出立即用冷水冲淋至完全凉透,沥水备用" + }, + { + "step": 3, + "description": "五花肉切成薄片,大蒜白色切下用刀背拍扁,小红辣椒切成段" + }, + { + "step": 4, + "description": "锅烧热放油,油热下大葱白爆香" + }, + { + "step": 5, + "description": "下五花肉片入锅,用中火煸炒至表面全部变色,继续煸炒一会儿,把肥肉部分的油份逼出一部分" + }, + { + "step": 6, + "description": "倒入红辣椒段和花菜,翻炒几下" + }, + { + "step": 7, + "description": "加入 10 ml 生抽" + }, + { + "step": 8, + "description": "再加入 5 g 白糖,转大火不断翻炒 1 分钟" + }, + { + "step": 9, + "description": "把大蒜叶部分切成段,放入锅中,翻炒几下后,关火盖上盖子焖 1 分钟即可" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-手撕包菜-手撕包菜", + "name": "手撕包菜的做法", + "description": "# 手撕包菜的做法\n\n手撕包菜是一道色香味俱全的汉族名菜,属于湘菜系\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/手撕包菜/手撕包菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/3.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/4.jpeg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "包菜", + "quantity": null, + "unit": null, + "text_quantity": "- 包菜", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "包菜", + "quantity": null, + "unit": null, + "text_quantity": "- 包菜 1 颗", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 200 g", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 2 根", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 60 ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 5 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 ml", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 5 ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 2 g", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 2 片", + "notes": "量未指定" + }, + { + "name": "蒜头", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜头 2 粒", + "notes": "量未指定" + }, + { + "name": "蒜苗", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜苗 0.5 根", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 5 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "包菜对半切开,去掉中间白色部分【参见图一】" + }, + { + "step": 2, + "description": "手撕包菜,碗中放入 2 g 盐,清洗包菜并沥干备用【参见图二】" + }, + { + "step": 3, + "description": "姜片、蒜头、小米辣、蒜苗处理后备用【参见图三】" + }, + { + "step": 4, + "description": "五花肉切片,清水清洗后备用" + }, + { + "step": 5, + "description": "锅中加入 30 ml 食用油,倒入包菜翻炒,大火翻炒 1 分钟 后,加入 3 g 盐 ,继续翻炒 2 分钟 后取出备用" + }, + { + "step": 6, + "description": "锅中加入 30 ml 食用油,倒入五花肉,大火翻炒 1 分钟" + }, + { + "step": 7, + "description": "倒入姜片等材料,翻炒 1 分钟" + }, + { + "step": 8, + "description": "倒入包菜翻炒后,加入 香醋、料酒、鸡精、料酒,大火继续翻炒,2 分钟 后出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-拔丝土豆-拔丝土豆", + "name": "拔丝土豆的做法", + "description": "# 拔丝土豆的做法\n\n拔丝土豆是一道色香味俱全的特色名菜,属于鲁菜系\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/拔丝土豆/拔丝土豆.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/3.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/4.jpeg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻", + "notes": "量未指定" + }, + { + "name": "土豆", + "quantity": null, + "unit": null, + "text_quantity": "- 土豆 2 个", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 300 ml (土豆能浮在油上面即可)", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 30 g", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 120 g (要多放糖,这是为了在土豆上面裹上一层厚厚的糖浆,从而产生拔丝的效果)", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 100 ml", + "notes": "量未指定" + }, + { + "name": "芝麻", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻 5 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "土豆去皮,切均匀的小块。放入淀粉(不加水)搅拌,使得淀粉覆盖土豆表面" + }, + { + "step": 2, + "description": "起锅烧油,放入土豆块,缓缓翻滚煎炸 5-7 分钟 ,直至筷子可以插进土豆" + }, + { + "step": 3, + "description": "取出土豆,放入大碗备用" + }, + { + "step": 4, + "description": "锅中加入水、白砂糖,沿着一个方向慢慢搅动白砂糖,直到白砂糖颜色变成褐色" + }, + { + "step": 5, + "description": "重新倒入土豆,翻炒 30 S 后 出锅" + }, + { + "step": 6, + "description": "土豆盛盘,散上芝麻" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-椒盐玉米-椒盐玉米", + "name": "椒盐玉米的做法", + "description": "# 椒盐玉米的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/椒盐玉米/椒盐玉米.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/椒盐玉米/椒盐玉米.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/椒盐玉米/椒盐玉米.jpeg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "玉米粒", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒", + "notes": "量未指定" + }, + { + "name": "椒盐", + "quantity": null, + "unit": null, + "text_quantity": "- 椒盐", + "notes": "量未指定" + }, + { + "name": "芝麻粒", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻粒", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "两个塑料簸箕", + "quantity": null, + "unit": null, + "text_quantity": "- 两个塑料簸箕", + "notes": "量未指定" + }, + { + "name": "若干吸油纸", + "quantity": null, + "unit": null, + "text_quantity": "- 若干吸油纸", + "notes": "量未指定" + }, + { + "name": "玉米粒(袋装)", + "quantity": null, + "unit": null, + "text_quantity": "- 玉米粒(袋装) 350g", + "notes": "量未指定" + }, + { + "name": "淀粉(在锅里完全能盖住玉米粒表面为准,在", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉(在锅里完全能盖住玉米粒表面为准,在 40 - 70g)", + "notes": "量未指定" + }, + { + "name": "椒盐粉", + "quantity": null, + "unit": null, + "text_quantity": "- 椒盐粉 10g", + "notes": "量未指定" + }, + { + "name": "芝麻粒", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻粒 10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "玉米粒都是剥好的,直接解冻即可,温水泡 15 分钟或者灶上开水煮 5 分钟。" + }, + { + "step": 2, + "description": "拿出一个簸箕,将其假设为 BoxA,垫上吸油纸,倒进解冻好的玉米粒。" + }, + { + "step": 3, + "description": "shaking shaking shaking! - 直到吸油纸全部变湿为止。" + }, + { + "step": 4, + "description": "拿出第二个簸箕 BoxB,垫上吸油纸,将 BoxA 的玉米粒全部倒入 BoxB 中。" + }, + { + "step": 5, + "description": "shaking shaking shaking! - 直到吸油纸全部变湿为止。" + }, + { + "step": 6, + "description": "重复上述操作多次,直到玉米表面没有明显可见的水滴但保持湿润的状态。" + }, + { + "step": 7, + "description": "倒入大量淀粉,能够完全盖住玉米粒。" + }, + { + "step": 8, + "description": "shaking shaking shaking! - 直到淀粉裹住了玉米粒" + }, + { + "step": 9, + "description": "开灶 - 放锅 - 倒入油 尽量铺满锅底 但不要太多。" + }, + { + "step": 10, + "description": "油热 8 成,倒入裹上了淀粉的玉米粒。" + }, + { + "step": 11, + "description": "中火先煎 30s,不要翻炒,不然淀粉会掉。" + }, + { + "step": 12, + "description": "轻微翻炒 3 分钟即可出锅。" + }, + { + "step": 13, + "description": "最重要的一步:撒上 3g 椒盐,撒上芝麻粒!" + }, + { + "step": 14, + "description": "香喷喷的”椒盐玉米“就做好了" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-榄菜肉末四季豆-榄菜肉末四季豆", + "name": "榄菜肉末四季豆的做法", + "description": "# 榄菜肉末四季豆的做法\n\n![榄菜肉末四季豆成品](./榄菜肉末四季豆.JPG)\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/榄菜肉末四季豆/榄菜肉末四季豆.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/榄菜肉末四季豆/榄菜肉末四季豆.JPG", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/榄菜肉末四季豆/榄菜肉末四季豆.JPG" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "四季豆", + "quantity": null, + "unit": null, + "text_quantity": "- 四季豆", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉", + "notes": "量未指定" + }, + { + "name": "橄榄菜", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄菜", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "小米辣(不吃辣可以不放)", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣(不吃辣可以不放)", + "notes": "量未指定" + }, + { + "name": "四季豆", + "quantity": null, + "unit": null, + "text_quantity": "- 四季豆 220g", + "notes": "量未指定" + }, + { + "name": "五花肉", + "quantity": null, + "unit": null, + "text_quantity": "- 五花肉 100g", + "notes": "量未指定" + }, + { + "name": "橄榄菜", + "quantity": null, + "unit": null, + "text_quantity": "- 橄榄菜 20g", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 10g", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 10g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将四季豆洗净,并把筋撕干净,然后切成大小均匀的颗粒备用。" + }, + { + "step": 2, + "description": "将大蒜拍碎剁成蒜末备用。" + }, + { + "step": 3, + "description": "将小米辣切成大小均匀的颗粒备用。" + }, + { + "step": 4, + "description": "将五花肉去皮,然后剁成肉末备用。" + }, + { + "step": 5, + "description": "将锅烧热,然后加入 20ml 油滑锅,锅滑好之后将热油倒出,然后加入 10ml 冷油,这就是传说中热锅冷油,这么做主要是防止肉末粘锅。" + }, + { + "step": 6, + "description": "如果家里没有晾油瓶的话,也可以不用滑锅,放入油之后,直接加入肉末开始煸炒,小火煸炒两分钟,炒出猪油。" + }, + { + "step": 7, + "description": "肉末炒香之后加入蒜末,橄榄菜和小米辣,炒出香味。" + }, + { + "step": 8, + "description": "加入四季豆开中火煸炒,四季豆至少要炒 5 分钟,一定要保证四季豆**熟透**,否则可能会食物中毒。" + }, + { + "step": 9, + "description": "四季豆炒熟后加入 2ml 酱油从锅边淋入,然后加入 2g 盐、1g 鸡精、1g 胡椒粉和 0.5g 糖。" + }, + { + "step": 10, + "description": "将调料翻炒均匀。" + }, + { + "step": 11, + "description": "出锅,装盘。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-洋葱炒鸡蛋-洋葱炒鸡蛋", + "name": "洋葱炒鸡蛋的做法", + "description": "# 洋葱炒鸡蛋的做法\n\n洋葱炒鸡蛋,是中国的一道日常生活中所熟知的菜品\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/洋葱炒鸡蛋/洋葱炒鸡蛋.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/洋葱炒鸡蛋/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/洋葱炒鸡蛋/1.jpeg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "洋葱", + "quantity": null, + "unit": null, + "text_quantity": "- 洋葱 50 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 50 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 g", + "notes": "量未指定" + }, + { + "name": "葱 半 根", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 半 根", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 2 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋打入大碗中,加入洋葱片、盐后搅拌 60 S" + }, + { + "step": 2, + "description": "起锅烧油,倒入鸡蛋,一面煎炸 30-45 S ,翻面继续翻炒,反复 2-3 分钟 后散上料酒出锅" + }, + { + "step": 3, + "description": "鸡蛋装盘,散上葱花" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-炒滑蛋-炒滑蛋", + "name": "炒滑蛋的做法", + "description": "# 炒滑蛋的做法\n\n![炒滑蛋成品](./炒滑蛋.jpg)\n\n炒滑蛋是一道简单易做的菜。一般初学者只需要 5 分钟即可完成。\n\n预估烹饪难度:★", + "source_path": "dishes/vegetable_dish/炒滑蛋/炒滑蛋.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/炒滑蛋/炒滑蛋.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/炒滑蛋/炒滑蛋.jpg" + ], + "category": "素菜", + "difficulty": 1, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋(最好是无菌蛋)", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋(最好是无菌蛋)", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 4 颗", + "notes": "量未指定" + }, + { + "name": "牛奶", + "quantity": null, + "unit": null, + "text_quantity": "- 牛奶 30ml", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "鸡蛋加入牛奶以及 5ml 食用油搅拌均匀,备用" + }, + { + "step": 2, + "description": "大火烧热平底锅约 30s, 加入 5ml 食用油" + }, + { + "step": 3, + "description": "烧 30s 转小火, 并且放入搅拌好的鸡蛋" + }, + { + "step": 4, + "description": "在锅中静置 5 秒后,用锅铲将蛋液从边缘缓慢推向中间" + }, + { + "step": 5, + "description": "翻炒至鸡蛋大致凝固后关火,装盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-烤茄子-烤茄子", + "name": "烤茄子的做法", + "description": "# 烤茄子的做法\n\n非常简单方便,而且香极了\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/烤茄子/烤茄子.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/烤茄子/烤茄子.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/烤茄子/烤茄子.jpg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "酱油(生抽)", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油(生抽)", + "notes": "量未指定" + }, + { + "name": "蒜蓉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜蓉", + "notes": "量未指定" + }, + { + "name": "辣椒", + "quantity": null, + "unit": null, + "text_quantity": "- 辣椒", + "notes": "量未指定" + }, + { + "name": "孜然", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "茄子", + "quantity": null, + "unit": null, + "text_quantity": "- 茄子 1 个 (大约 200g)", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 20-30 毫升", + "notes": "量未指定" + }, + { + "name": "酱油", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油 4-6 克", + "notes": "量未指定" + }, + { + "name": "小米椒", + "quantity": null, + "unit": null, + "text_quantity": "- 小米椒 1 个 (大约 20g)", + "notes": "量未指定" + }, + { + "name": "蒜蓉", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜蓉 3-4 瓣", + "notes": "量未指定" + }, + { + "name": "孜然", + "quantity": null, + "unit": null, + "text_quantity": "- 孜然 1-3 克", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 0.5-2 克", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将酱油、孜然、食用盐、蒜蓉和切碎的小米椒置于碗中,均匀搅拌备用" + }, + { + "step": 2, + "description": "茄子洗净,用纸巾擦干表面的水分" + }, + { + "step": 3, + "description": "用叉子在茄子的一侧扎 4-8 下" + }, + { + "step": 4, + "description": "使用 15-25ml 的食用油涂满茄子表面" + }, + { + "step": 5, + "description": "将烤箱温度设置为 200℃ (打开烤箱风扇 大火),预热 2 分钟" + }, + { + "step": 6, + "description": "将茄子放入烤箱中层或者上层,烤制 12-15 分钟 (茄子表面有褶皱,且能按压 0.3-0.5cm 的深度即可)" + }, + { + "step": 7, + "description": "取出茄子,用刀茄子上竖着划一个口子。口子居中,上下距 1-1.5cm" + }, + { + "step": 8, + "description": "用小刀或者叉子伸入口子,竖着切割茄子内部" + }, + { + "step": 9, + "description": "将口子微微掰开,倒入第一步准备的酱料" + }, + { + "step": 10, + "description": "再次将茄子放入烤箱,将烤箱温度设置为 200℃ ,烤制 4-7 分钟" + }, + { + "step": 11, + "description": "取出,关闭烤箱电源" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-白灼菜心-白灼菜心", + "name": "白灼菜心的做法", + "description": "# 白灼菜心的做法\n\n\n\n![白灼菜心](./白灼菜心.jpg)\n\n> 没有拍照,上图是网图,不过做出来都差不多啦\n\n白灼菜心是经典粤菜,白灼是粤菜的一种烹饪技法,用煮沸的水或汤将生的食物烫熟,称为白灼。这种烹饪手法能保持原有的鲜味,粤菜常用此法烹制虾和蔬菜。\n\n总之吧,减肥或者是**快速解决绿叶菜的绝佳方式**。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/白灼菜心/白灼菜心.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/白灼菜心/白灼菜心.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/白灼菜心/白灼菜心.jpg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "新鲜菜心", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜菜心", + "notes": "量未指定" + }, + { + "name": "生抽、蚝油、盐", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽、蚝油、盐", + "notes": "量未指定" + }, + { + "name": "大蒜、小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜、小米辣", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "新鲜菜心", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜菜心 250g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10g", + "notes": "量未指定" + }, + { + "name": "调一个灵魂料汁儿", + "quantity": null, + "unit": null, + "text_quantity": "- 调一个灵魂料汁儿", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5g", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5g", + "notes": "量未指定" + }, + { + "name": "盐、糖", + "quantity": null, + "unit": null, + "text_quantity": "- 盐、糖 5g", + "notes": "量未指定" + }, + { + "name": "大蒜四五瓣、小米辣一两根", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜四五瓣、小米辣一两根", + "notes": "量未指定" + } + ], + "steps": [], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-糖拌西红柿-糖拌西红柿", + "name": "糖拌西红柿的做法", + "description": "# 糖拌西红柿的做法\n\n![示例菜成品](./火山飘雪.jpg)\n\n新鲜可口,制作简便,营养价值高,适合夏季食用,家庭餐桌上的一道美味凉菜。西红柿含有大量的维生素 C, 做法简单 几分钟就可完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/糖拌西红柿/糖拌西红柿.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/糖拌西红柿/火山飘雪.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/糖拌西红柿/火山飘雪.jpg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖", + "notes": "量未指定" + }, + { + "name": "冰箱", + "quantity": null, + "unit": null, + "text_quantity": "- 冰箱", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 2 个(每个西红柿约 100g,共 200g)", + "notes": "量未指定" + }, + { + "name": "白砂糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白砂糖 20g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "用刀将西红柿皮米字型划开" + }, + { + "step": 2, + "description": "用筷子插入西红柿的菊花,在燃气上转动烤 10 秒(或用开水冲 30 秒),直到西红柿皮卷边" + }, + { + "step": 3, + "description": "把西红柿的衣服脱光" + }, + { + "step": 4, + "description": "再西红柿大卸八块(沿纹路切可以更多的留汁水),去掉头部根蒂部,备用" + }, + { + "step": 5, + "description": "全部切好后,将西红柿在盘子中均匀码一层" + }, + { + "step": 6, + "description": "撒上白糖,重复上面一步直到全部西红柿放完" + }, + { + "step": 7, + "description": "放入冰箱冷藏 10 分钟" + }, + { + "step": 8, + "description": "一盘糖拌西红柿就好了,营养美味,酸甜爽口,夏日解暑又解腻" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-红烧冬瓜-红烧冬瓜", + "name": "红烧冬瓜的做法", + "description": "# 红烧冬瓜的做法\n\n红烧冬瓜是一道具有色泽红亮,香鲜味美、营养价值丰富的家常菜\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/红烧冬瓜/红烧冬瓜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/红烧冬瓜/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/红烧冬瓜/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/红烧冬瓜/2.jpeg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "冬瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 冬瓜", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "冬瓜", + "quantity": null, + "unit": null, + "text_quantity": "- 冬瓜 300 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 50 ml", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 2 ml", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 10 g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10 ml", + "notes": "量未指定" + }, + { + "name": "老抽", + "quantity": null, + "unit": null, + "text_quantity": "- 老抽 15 ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 3 g", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 0.5 根", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末 1 粒", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 15 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "冬瓜去皮,切 边长不超过 2cm 小块" + }, + { + "step": 2, + "description": "起锅烧油,放入冬瓜,缓缓翻滚煎炸 2 分钟 ,直至冬瓜表面泛金黄色" + }, + { + "step": 3, + "description": "取出冬瓜,放入大碗备用" + }, + { + "step": 4, + "description": "利用锅中的剩余油,依次放入姜末、生抽、蚝油,翻炒 15 S" + }, + { + "step": 5, + "description": "重新倒入冬瓜,翻炒 30 S 后,加入开水,水要没过冬瓜表面,大火煮 10 分钟" + }, + { + "step": 6, + "description": "加入老抽上色,继续煮,直至冬瓜软糯(筷子可以轻松插近冬瓜)" + }, + { + "step": 7, + "description": "加入鸡精、料酒、香葱翻炒后 30 S, 取出冬瓜到大碗中" + }, + { + "step": 8, + "description": "锅中剩余汤汁保留,倒入水淀粉,煮开后汤汁浇灌在冬瓜表面" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-芹菜拌茶树菇-芹菜拌茶树菇", + "name": "芹菜拌茶树菇的做法", + "description": "# 芹菜拌茶树菇的做法\n\n![芹菜拌茶树菇成品](./芹菜拌茶树菇.jpg)\n![闽星茶树菇](./闽星茶树菇.jpg)\n\n芹菜拌茶树菇是一道简单易做的凉拌菜。富含多种人体所需的维生素和矿物质。一般初学者只需要 30 分钟即可完成。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/芹菜拌茶树菇/芹菜拌茶树菇.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/芹菜拌茶树菇/芹菜拌茶树菇.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/芹菜拌茶树菇/芹菜拌茶树菇.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/芹菜拌茶树菇/闽星茶树菇.jpg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "闽星茶树菇", + "quantity": null, + "unit": null, + "text_quantity": "- 闽星茶树菇", + "notes": "量未指定" + }, + { + "name": "芹菜", + "quantity": null, + "unit": null, + "text_quantity": "- 芹菜", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "味极鲜", + "quantity": null, + "unit": null, + "text_quantity": "- 味极鲜", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "闽星茶树菇", + "quantity": null, + "unit": null, + "text_quantity": "- 闽星茶树菇 1 瓶", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油 5ml", + "notes": "量未指定" + }, + { + "name": "蚝油 大约", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 大约 7ml", + "notes": "量未指定" + }, + { + "name": "味极鲜", + "quantity": null, + "unit": null, + "text_quantity": "- 味极鲜 3ml", + "notes": "量未指定" + }, + { + "name": "食用盐 大约", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 大约 2g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "(如果是芹菜苗这一步略过)用热水壶烧一壶热水,备用" + }, + { + "step": 2, + "description": "新鲜的芹菜苗或者芹菜摘去黄叶清洗,备用" + }, + { + "step": 3, + "description": "(如果是芹菜苗这一步略过)将芹菜摘去叶子单独放一个盆中,将芹菜茎用刀划成 2-3 毫米宽的芹菜条备用,这一步的目的是让芹菜断生的更快更均匀,吃起来更脆更爽口" + }, + { + "step": 4, + "description": "芹菜苗切成 4cm 的芹菜段,备用" + }, + { + "step": 5, + "description": "(如果是芹菜苗这一步略过)起锅开火,将热水壶的开水倒入锅中待水起泡沸腾" + }, + { + "step": 6, + "description": "(如果是芹菜苗这一步略过)将切好的芹菜条放入锅中焯水,大约 20 秒放入芹菜叶,5 秒后关火全部捞出过凉水,备用" + }, + { + "step": 7, + "description": "将盆中焯好的芹菜或者芹菜苗撒上准备好的食盐,香油,耗油和味极鲜搅拌均匀" + }, + { + "step": 8, + "description": "将茶树菇倒入盆中搅拌均匀" + }, + { + "step": 9, + "description": "装盘" + }, + { + "step": 10, + "description": "开吃" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-莴笋叶煎饼-莴笋叶煎饼", + "name": "莴笋叶煎饼的做法", + "description": "# 莴笋叶煎饼的做法\n\n莴笋叶煎饼营养、好吃\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/莴笋叶煎饼/莴笋叶煎饼.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/2.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/3.jpeg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "莴笋叶", + "quantity": null, + "unit": null, + "text_quantity": "- 莴笋叶", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "莴笋叶", + "quantity": null, + "unit": null, + "text_quantity": "- 莴笋叶 50 g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 30 ml", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 5 ml", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 15 g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 2 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "莴笋叶剁碎,加入鸡蛋、生粉、生抽、鸡精搅拌均匀备用" + }, + { + "step": 2, + "description": "起锅烧油,倒入莴笋叶浆汁,均匀平铺在锅面上" + }, + { + "step": 3, + "description": "第一面炸 120 S 后,翻面再炸 60 S 后出锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-菠菜炒鸡蛋-菠菜炒鸡蛋", + "name": "菠菜炒鸡蛋的做法", + "description": "# 菠菜炒鸡蛋的做法\n\n这道菜难度系数简单,营养丰富。\n\n![示例菜成品](./菠菜炒鸡蛋.jpg)\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/菠菜炒鸡蛋/菠菜炒鸡蛋.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/菠菜炒鸡蛋/菠菜炒鸡蛋.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/菠菜炒鸡蛋/菠菜炒鸡蛋.jpg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "菠菜", + "quantity": null, + "unit": null, + "text_quantity": "- 菠菜", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "菠菜", + "quantity": null, + "unit": null, + "text_quantity": "- 菠菜 350g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 15ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 5g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "菠菜去根,洗净,放在篮子里,焯水" + }, + { + "step": 2, + "description": "将鸡蛋打入碗中,搅匀" + }, + { + "step": 3, + "description": "热锅,加入 10ml 油" + }, + { + "step": 4, + "description": "油热后,倒入鸡蛋液,中火翻炒 15 秒,先煎成蛋饼,然后再用锅铲切成小块" + }, + { + "step": 5, + "description": "关火,将鸡蛋块 盛到盘子中,不要洗锅" + }, + { + "step": 6, + "description": "重新开火,倒入 5ml 油,油热后,放入菠菜,大火 翻炒 15 秒后,倒入鸡蛋块,翻炒均匀" + }, + { + "step": 7, + "description": "加入 5g 盐、100ml 饮用水,大火 翻炒 10 秒" + }, + { + "step": 8, + "description": "关火,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-蒜蓉空心菜-蒜蓉空心菜", + "name": "蒜蓉空心菜的做法", + "description": "# 蒜蓉空心菜的做法\n\n背景:\n\n曾经去学校附近的川菜馆吃过蒜蓉空心菜,之后就一直很喜欢吃。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/蒜蓉空心菜/蒜蓉空心菜.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蒜蓉空心菜/1.JPG", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蒜蓉空心菜/1.JPG" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "空心菜", + "quantity": null, + "unit": null, + "text_quantity": "- 空心菜", + "notes": "量未指定" + }, + { + "name": "蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 蒜末", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "筷子", + "quantity": null, + "unit": null, + "text_quantity": "- 筷子", + "notes": "量未指定" + }, + { + "name": "铲子", + "quantity": null, + "unit": null, + "text_quantity": "- 铲子", + "notes": "量未指定" + }, + { + "name": "新鲜空心菜", + "quantity": null, + "unit": null, + "text_quantity": "- 新鲜空心菜 250 g", + "notes": "量未指定" + }, + { + "name": "大蒜半个,切碎为蒜末", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜半个,切碎为蒜末", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 45 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 g", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 3 g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 8 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "空心菜洗净,去掉烂叶或者老梗,均匀切成 2 段或者 3 段(防止过长不好炒)" + }, + { + "step": 2, + "description": "锅里先倒少量油,烧至微微冒烟,此时拿起锅将国内的热油向四周浸润,让油均匀覆盖锅底,然后再倒入剩余的油([热锅凉油法](https://cook.aiursoft.cn/tips/learn/%E5%AD%A6%E4%B9%A0%E7%82%92%E4%B8%8E%E7%85%8E/?h=%E7%83%AD%E9%94%85#_5))。" + }, + { + "step": 3, + "description": "放入蒜末,小火炒 10 到 15 秒煸香" + }, + { + "step": 4, + "description": "尽快均匀地放入空心菜,**开大火**,左手拿铲子,右手拿筷子,配合将空心菜不停翻动,**直至软化变绿**。" + }, + { + "step": 5, + "description": "接着不需使用筷子,而是使用铲子快速翻炒已软化的空心菜 15 - 20 秒,使之受热更均匀,撒入盐 2 g ,白糖 3 g,生抽 8 ml。" + }, + { + "step": 6, + "description": "继续大火翻炒 10 秒,即可出锅。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-虎皮青椒-虎皮青椒", + "name": "虎皮青椒的做法", + "description": "# 虎皮青椒的做法\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/虎皮青椒/虎皮青椒.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/虎皮青椒/虎皮青椒.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/虎皮青椒/虎皮青椒.jpg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜", + "notes": "量未指定" + }, + { + "name": "白糖(灵魂)", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖(灵魂)", + "notes": "量未指定" + }, + { + "name": "醋", + "quantity": null, + "unit": null, + "text_quantity": "- 醋", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "砵或者有一定深度的碗", + "quantity": null, + "unit": null, + "text_quantity": "- 砵或者有一定深度的碗", + "notes": "量未指定" + }, + { + "name": "青椒", + "quantity": null, + "unit": null, + "text_quantity": "- 青椒 5 个,长度在 10-15cm 的最为合适", + "notes": "量未指定" + }, + { + "name": "大蒜", + "quantity": null, + "unit": null, + "text_quantity": "- 大蒜 2-3 瓣", + "notes": "量未指定" + }, + { + "name": "油", + "quantity": null, + "unit": null, + "text_quantity": "- 油 20ml", + "notes": "量未指定" + }, + { + "name": "白糖", + "quantity": null, + "unit": null, + "text_quantity": "- 白糖 15g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 15ml", + "notes": "量未指定" + }, + { + "name": "香醋", + "quantity": null, + "unit": null, + "text_quantity": "- 香醋 15ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 4g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "去掉青椒蒂,用自来水冲洗干净。" + }, + { + "step": 2, + "description": "青椒切长片,平均一个青椒纵向切成 3-4 片即可。" + }, + { + "step": 3, + "description": "大蒜去皮,切成碎末,体积在 2mm x 2mm x 2mm 即可。" + }, + { + "step": 4, + "description": "`调料 1`:拿一个小碗倒入 20ml 油,将大蒜末放入其中。" + }, + { + "step": 5, + "description": "`调料 2`:白糖、生抽、醋、盐全部倒入砵(碗)等容器,搅拌。" + }, + { + "step": 6, + "description": "将 `调料 1` 倒入锅中,开火加热 5 成放入青椒,青椒片不要叠在一起,单独成片放置锅中。" + }, + { + "step": 7, + "description": "用锅铲不停的按压青椒,合适的时候翻面。" + }, + { + "step": 8, + "description": "翻炒约 2 分钟,待青椒表皮出现褶皱时,倒入 `调料 2`。" + }, + { + "step": 9, + "description": "加大火候继续翻炒 30s 后即可出锅盛入盘中。" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-蚝油三鲜菇-蚝油三鲜菇", + "name": "蚝油三鲜菇的做法", + "description": "# 蚝油三鲜菇的做法\n\n几分钟就能做出的蚝油蘑菇,滑嫩入味鲜美可口,别提多好吃了。\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/蚝油三鲜菇/蚝油三鲜菇.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蚝油三鲜菇/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蚝油三鲜菇/1.jpeg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蚝油三鲜菇/2.jpeg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 香菇", + "notes": "量未指定" + }, + { + "name": "蟹味菇", + "quantity": null, + "unit": null, + "text_quantity": "- 蟹味菇", + "notes": "量未指定" + }, + { + "name": "白玉菇", + "quantity": null, + "unit": null, + "text_quantity": "- 白玉菇", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣", + "notes": "量未指定" + }, + { + "name": "菜椒", + "quantity": null, + "unit": null, + "text_quantity": "- 菜椒", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油", + "notes": "量未指定" + }, + { + "name": "西蓝花", + "quantity": null, + "unit": null, + "text_quantity": "- 西蓝花", + "notes": "量未指定" + }, + { + "name": "鲜香菇", + "quantity": null, + "unit": null, + "text_quantity": "- 鲜香菇 2 朵", + "notes": "量未指定" + }, + { + "name": "蟹味菇", + "quantity": null, + "unit": null, + "text_quantity": "- 蟹味菇 30 g", + "notes": "量未指定" + }, + { + "name": "白玉菇", + "quantity": null, + "unit": null, + "text_quantity": "- 白玉菇 30 g", + "notes": "量未指定" + }, + { + "name": "小米辣", + "quantity": null, + "unit": null, + "text_quantity": "- 小米辣 1 根", + "notes": "量未指定" + }, + { + "name": "菜椒", + "quantity": null, + "unit": null, + "text_quantity": "- 菜椒 0.5 颗", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10 ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 5 g", + "notes": "量未指定" + }, + { + "name": "料酒", + "quantity": null, + "unit": null, + "text_quantity": "- 料酒 2 ml", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 10 g", + "notes": "量未指定" + }, + { + "name": "生抽", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 10 ml", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 3 g", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 0.5 根", + "notes": "量未指定" + }, + { + "name": "姜末", + "quantity": null, + "unit": null, + "text_quantity": "- 姜末 1 粒", + "notes": "量未指定" + }, + { + "name": "蚝油", + "quantity": null, + "unit": null, + "text_quantity": "- 蚝油 5 ml", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 350 ml", + "notes": "量未指定" + }, + { + "name": "西蓝花", + "quantity": null, + "unit": null, + "text_quantity": "- 西蓝花 100 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "蟹味菇、白玉菇 去掉根部泥土,掰散菌朵" + }, + { + "step": 2, + "description": "香菇切片(每片厚度 0.5-1 cm,厚点相对薄点更有嚼劲)" + }, + { + "step": 3, + "description": "生粉倒入小碗中,加入 50ml 水,搅拌生粉直至融化没有颗粒(即水淀粉)备用" + }, + { + "step": 4, + "description": "水开,放入西蓝花,清水煮 3 分钟,放入碗中备用" + }, + { + "step": 5, + "description": "洗锅烧开水,加入 5 g 食用盐,倒入蟹味菇、白玉菇、香菇,水煮 1 分钟" + }, + { + "step": 6, + "description": "1 分钟后,捞出沥干水分" + }, + { + "step": 7, + "description": "起锅烧油,待油开始冒小泡,放入姜末、小米辣、菜椒 煸炒 30 S" + }, + { + "step": 8, + "description": "倒入三鲜菇,然后依次倒入生抽、蚝油、鸡精,翻炒均匀后,倒入水淀粉" + }, + { + "step": 9, + "description": "中火烧干汁,加入料酒、葱花 出锅" + }, + { + "step": 10, + "description": "摆上西蓝花" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-西红柿豆腐汤羹-西红柿豆腐汤羹", + "name": "西红柿豆腐汤羹的做法", + "description": "# 西红柿豆腐汤羹的做法\n\n西红柿豆腐汤羹是一道很清淡美味的汤羹\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/西红柿豆腐汤羹/西红柿豆腐汤羹.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西红柿豆腐汤羹/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西红柿豆腐汤羹/1.jpeg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 豆腐", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 1 个", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "豆腐", + "quantity": null, + "unit": null, + "text_quantity": "- 豆腐 100 g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 5 ml", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 2 g", + "notes": "量未指定" + }, + { + "name": "淀粉", + "quantity": null, + "unit": null, + "text_quantity": "- 淀粉 5 g", + "notes": "量未指定" + }, + { + "name": "鸡精", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡精 2 g", + "notes": "量未指定" + }, + { + "name": "香葱", + "quantity": null, + "unit": null, + "text_quantity": "- 香葱 0.5 根", + "notes": "量未指定" + }, + { + "name": "姜", + "quantity": null, + "unit": null, + "text_quantity": "- 姜 1 片", + "notes": "量未指定" + }, + { + "name": "开水", + "quantity": null, + "unit": null, + "text_quantity": "- 开水 350 ml", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "西红柿切成小丁、鸡蛋打入碗中搅拌、豆腐切块备用" + }, + { + "step": 2, + "description": "起锅烧油,放入姜片 5 S 后倒入入西红柿翻炒 30 S" + }, + { + "step": 3, + "description": "锅中加入开水,汤水烧开,60 S 后到入鸡蛋液、豆腐块" + }, + { + "step": 4, + "description": "汤水重新烧开后,加入水淀粉,沿一个方向搅拌 2 圈" + }, + { + "step": 5, + "description": "加入鸡精、盐、香葱,30 S 后起锅" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-西葫芦炒鸡蛋-西葫芦炒鸡蛋", + "name": "西葫芦炒鸡蛋的做法", + "description": "# 西葫芦炒鸡蛋的做法\n\n![西葫芦炒鸡蛋](./西葫芦炒鸡蛋.jpeg)\n\n西葫芦炒鸡蛋是一道简单易做的家常菜。简单易购的食材,好吃又下饭。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/西葫芦炒鸡蛋/西葫芦炒鸡蛋.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西葫芦炒鸡蛋/西葫芦炒鸡蛋.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西葫芦炒鸡蛋/西葫芦炒鸡蛋.jpeg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "西葫芦", + "quantity": null, + "unit": null, + "text_quantity": "- 西葫芦", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "西红柿(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿(可选)", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "西葫芦", + "quantity": null, + "unit": null, + "text_quantity": "- 西葫芦 500g", + "notes": "量未指定" + }, + { + "name": "西红柿", + "quantity": null, + "unit": null, + "text_quantity": "- 西红柿 100g", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 3 个", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 10-20ml", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 6g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "西红柿洗净,切成小块,备用" + }, + { + "step": 2, + "description": "西葫芦洗净,切成边长约为 4cm 的菱形,备用" + }, + { + "step": 3, + "description": "打三个鸡蛋到碗里,打散搅匀,备用" + }, + { + "step": 4, + "description": "热锅,锅内放入 5ml - 10ml 食用油" + }, + { + "step": 5, + "description": "倒入鸡蛋,保持翻炒至鸡蛋成固体,用锅铲分成小块后盛到碗里,备用" + }, + { + "step": 6, + "description": "锅内放入 5ml - 10ml 食用油,倒入西红柿,炒至变软" + }, + { + "step": 7, + "description": "倒入西葫芦一起翻炒均匀,放入 6g 食用盐,将火调小然后**等待 4 - 5 分钟**" + }, + { + "step": 8, + "description": "倒入备用的鸡蛋,中火翻炒 15 秒" + }, + { + "step": 9, + "description": "关火,盛盘" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-话梅煮毛豆-话梅煮毛豆", + "name": "话梅煮毛豆的做法", + "description": "# 话梅煮毛豆的做法\n\n酸甜可口、营养价值高的一种简易美食\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/话梅煮毛豆/话梅煮毛豆.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/话梅煮毛豆/1.jpeg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/话梅煮毛豆/1.jpeg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "毛豆", + "quantity": null, + "unit": null, + "text_quantity": "- 毛豆", + "notes": "量未指定" + }, + { + "name": "话梅", + "quantity": null, + "unit": null, + "text_quantity": "- 话梅", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "毛豆", + "quantity": null, + "unit": null, + "text_quantity": "- 毛豆 300 g", + "notes": "量未指定" + }, + { + "name": "话梅", + "quantity": null, + "unit": null, + "text_quantity": "- 话梅 6 颗", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 2 g", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "清水加入食用盐,毛豆浸泡 15 分钟" + }, + { + "step": 2, + "description": "加入开水,倒入毛豆、话梅,水煮 20-30 分钟" + }, + { + "step": 3, + "description": "起锅开吃" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-鸡蛋羹-微波炉鸡蛋羹", + "name": "微波炉鸡蛋羹的做法", + "description": "# 微波炉鸡蛋羹的做法\n\n![微波炉鸡蛋羹](./微波炉鸡蛋羹.jpg)\n微波炉鸡蛋羹是一个简单易制作的菜。非常适合夜间突然饿了的时候充当夜宵,快捷简单。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.jpg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个 * 份数", + "notes": "量未指定" + }, + { + "name": "水", + "quantity": null, + "unit": null, + "text_quantity": "- 水 200ml * 份数", + "notes": "量未指定" + }, + { + "name": "虾皮", + "quantity": null, + "unit": null, + "text_quantity": "- 虾皮 10 个 * 份数(可选)", + "notes": "量未指定" + }, + { + "name": "葱", + "quantity": null, + "unit": null, + "text_quantity": "- 葱 5g *份数(可选)", + "notes": "量未指定" + }, + { + "name": "盐", + "quantity": null, + "unit": null, + "text_quantity": "- 盐 3g * 份数", + "notes": "量未指定" + }, + { + "name": "酱油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 酱油(可选)", + "notes": "量未指定" + }, + { + "name": "芝麻油(香油)", + "quantity": null, + "unit": null, + "text_quantity": "- 芝麻油(香油) 1ml(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "将鸡蛋打入可使用微波炉加热的陶瓷碗中,使用筷子将其打散。" + }, + { + "step": 2, + "description": "加入水和盐,搅拌均匀。" + }, + { + "step": 3, + "description": "将虾皮放入碗中,搅拌均匀,保证所有虾皮不会堆积在一起。" + }, + { + "step": 4, + "description": "葱切碎至边长 0.6±3mm 状,放入碗中搅拌均匀。" + }, + { + "step": 5, + "description": "将此碗及内容物放入微波炉中,容器表面覆盖保鲜膜或以可微波瓷盘加盖(注意:不得密封,必须留有涨缩量)加热 2 分钟(500W)。" + }, + { + "step": 6, + "description": "小心地取下保鲜膜或其他覆盖物,然后继续加热 2 分钟。" + }, + { + "step": 7, + "description": "若微波炉不带旋转式加热盘,将碗缓慢的水平旋转 180 度,以确保内容物加热均匀。" + }, + { + "step": 8, + "description": "放入芝麻油。" + }, + { + "step": 9, + "description": "小心地从微波炉中拿出碗(真的很烫)。" + }, + { + "step": 10, + "description": "如果选择放入酱油,则确保酱油在鸡蛋羹表面流动后能以最薄的形式沾满鸡蛋羹表面即可。" + }, + { + "step": 11, + "description": "开心的享受鸡蛋羹" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-鸡蛋羹-蒸箱鸡蛋羹", + "name": "蒸箱鸡蛋羹的做法", + "description": "# 蒸箱鸡蛋羹的做法\n\n蒸箱鸡蛋羹,是一道简单快捷易做的菜,制作时长约为 15 分钟。适用于有家庭蒸箱的厨师。\n\n预估烹饪难度:★★★", + "source_path": "dishes/vegetable_dish/鸡蛋羹/蒸箱鸡蛋羹.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.jpg" + ], + "category": "素菜", + "difficulty": 3, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油", + "notes": "量未指定" + }, + { + "name": "生抽(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽(可选)", + "notes": "量未指定" + }, + { + "name": "蒸箱", + "quantity": null, + "unit": null, + "text_quantity": "- 蒸箱", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 1 个", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 1g", + "notes": "量未指定" + }, + { + "name": "食用油", + "quantity": null, + "unit": null, + "text_quantity": "- 食用油 5ml", + "notes": "量未指定" + }, + { + "name": "生抽 / 味极鲜", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 / 味极鲜 6ml(可选调味)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "一个鸡蛋放入碗中打散" + }, + { + "step": 2, + "description": "向碗中加入鸡蛋体积 1.0-1.5 倍 60 度纯净水,并且搅拌均匀" + }, + { + "step": 3, + "description": "加入食用盐 1g" + }, + { + "step": 4, + "description": "加入食用油 5ml" + }, + { + "step": 5, + "description": "过滤蛋液,去掉蛋液中的浮沫(可选,不过滤蒸出来的蛋会有气泡导致不好看)" + }, + { + "step": 6, + "description": "确认蒸箱的水源已经补充至足够(若不确定,可把水槽补满)" + }, + { + "step": 7, + "description": "将已经完全搅拌均匀的鸡蛋液碗放入蒸箱" + }, + { + "step": 8, + "description": "调节至**100摄氏度**蒸 **10 分钟**" + }, + { + "step": 9, + "description": "打开蒸箱 (注意:蒸箱在开启时会有蒸气瞬间喷出,注意缓慢开启)" + }, + { + "step": 10, + "description": "出锅(可加入生抽调味)" + }, + { + "step": 11, + "description": "享用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + }, + { + "id": "dishes-vegetable_dish-鸡蛋羹-鸡蛋羹", + "name": "鸡蛋羹的做法", + "description": "# 鸡蛋羹的做法\n\n![鸡蛋羹成品](./鸡蛋羹.jpg)\n\n鸡蛋羹,又称水蒸蛋,不需要准备复杂的食材,是一道简单快捷易做的菜,当早餐或是正餐都可,制作时长约为 15 分钟。\n\n预估烹饪难度:★★", + "source_path": "dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.md", + "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg", + "images": [ + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg", + "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.jpg" + ], + "category": "素菜", + "difficulty": 2, + "tags": [ + "素菜" + ], + "servings": 1, + "ingredients": [ + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐", + "notes": "量未指定" + }, + { + "name": "香油", + "quantity": null, + "unit": null, + "text_quantity": "- 香油", + "notes": "量未指定" + }, + { + "name": "生抽 / 味极鲜", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 / 味极鲜", + "notes": "量未指定" + }, + { + "name": "白醋(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋(可选)", + "notes": "量未指定" + }, + { + "name": "藤椒油(可选)", + "quantity": null, + "unit": null, + "text_quantity": "- 藤椒油(可选)", + "notes": "量未指定" + }, + { + "name": "鸡蛋", + "quantity": null, + "unit": null, + "text_quantity": "- 鸡蛋 2 个", + "notes": "量未指定" + }, + { + "name": "食用盐", + "quantity": null, + "unit": null, + "text_quantity": "- 食用盐 3g", + "notes": "量未指定" + }, + { + "name": "香油(或芝麻油)", + "quantity": null, + "unit": null, + "text_quantity": "- 香油(或芝麻油) 2-4ml", + "notes": "量未指定" + }, + { + "name": "生抽 / 味极鲜", + "quantity": null, + "unit": null, + "text_quantity": "- 生抽 / 味极鲜 8ml", + "notes": "量未指定" + }, + { + "name": "白醋(或料酒)", + "quantity": null, + "unit": null, + "text_quantity": "- 白醋(或料酒) 2ml(可选)", + "notes": "量未指定" + } + ], + "steps": [ + { + "step": 1, + "description": "两个鸡蛋放入碗中打散" + }, + { + "step": 2, + "description": "加入食用盐 3g" + }, + { + "step": 3, + "description": "加入 2ml 白醋,去除鸡蛋的腥味(可选)" + }, + { + "step": 4, + "description": "向碗中加入鸡蛋体积 1-1.5 倍的 70 度纯净水,并且搅拌均匀" + }, + { + "step": 5, + "description": "过滤蛋液,去掉蛋液中的浮沫(可选,不过滤蒸出来的蛋会有气泡导致不好看)" + }, + { + "step": 6, + "description": "向任意一口锅中加入 50ml 清水,水烧开后,放入盛有鸡蛋液的碗" + }, + { + "step": 7, + "description": "蒸煮步骤(二选一)" + }, + { + "step": 8, + "description": "如何判断已经熟了?" + }, + { + "step": 9, + "description": "出锅" + }, + { + "step": 10, + "description": "加入香油和生抽即可享用" + } + ], + "prep_time_minutes": null, + "cook_time_minutes": null, + "total_time_minutes": null, + "additional_notes": [ + "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。" + ] + } +] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/recipes.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/recipes.ts new file mode 100644 index 00000000..a5036d99 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/recipes.ts @@ -0,0 +1,36 @@ +import { Recipe } from '../types/index.js' +import localRecipes from './all_recipes.json' with { type: 'json' }; + +// 远程菜谱JSON文件URL +const RECIPES_URL = 'https://weilei.site/all_recipes.json' + +/** + * 从远程获取菜谱,如果失败则使用本地备份 + */ +export async function fetchRecipes(): Promise { + try { + const response = await fetch(RECIPES_URL) + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`) + } + const data = await response.json() + return data as Recipe[] + } catch (error) { + console.error('获取远程菜谱数据失败,将使用本地备份数据') + // localRecipes 已经是 JSON 转好的对象 + return localRecipes as Recipe[] + } +} + +/** + * 获取所有分类 + */ +export function getAllCategories(recipes: Recipe[]): string[] { + const categories = new Set() + recipes.forEach((recipe) => { + if (recipe.category) { + categories.add(recipe.category) + } + }) + return Array.from(categories) +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/index.ts new file mode 100644 index 00000000..3dadf61b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/index.ts @@ -0,0 +1,213 @@ +#!/usr/bin/env node + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { Command } from 'commander'; +import { createServer } from 'http'; +import { fetchRecipes, getAllCategories } from "./data/recipes.js"; +import { registerGetAllRecipesTool } from "./tools/getAllRecipes.js"; +import { registerGetRecipeByIdTool } from "./tools/getRecipeById.js"; +import { registerGetRecipesByCategoryTool } from "./tools/getRecipesByCategory.js"; +import { registerRecommendMealsTool } from "./tools/recommendMeals.js"; +import { registerWhatToEatTool } from "./tools/whatToEat.js"; +import { Recipe } from './types/index.js'; + +// 全局变量存储数据 +let recipes: Recipe[] = []; +let categories: string[] = []; + +// 命令行参数处理 +const program = new Command() + .option("--transport ", "transport type", "stdio") + .option("--port ", "port for HTTP/SSE transport", "3000") + .parse(process.argv); + +const cliOptions = program.opts<{ + transport: string; + port: string; +}>(); + +const allowedTransports = ["stdio", "http", "sse"]; +if (!allowedTransports.includes(cliOptions.transport)) { + console.error( + `Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http, sse.` + ); + process.exit(1); +} + +const TRANSPORT_TYPE = (cliOptions.transport || "stdio") as "stdio" | "http" | "sse"; +const PORT = parseInt(cliOptions.port, 10); +// SSE transports +const sseTransports: Record = {}; +// 创建MCP服务器实例 +function createServerInstance(): McpServer { + const server = new McpServer({ + name: 'howtocook-mcp', + version: '0.1.1', + }, { + capabilities: { + logging: {}, + }, + }); + + // 注册所有工具 + registerGetAllRecipesTool(server, recipes); + registerGetRecipesByCategoryTool(server, recipes, categories); + registerRecommendMealsTool(server, recipes); + registerWhatToEatTool(server, recipes); + registerGetRecipeByIdTool(server, recipes); + + return server; +} + +// 加载菜谱数据 +async function loadRecipeData() { + try { + recipes = await fetchRecipes(); + categories = getAllCategories(recipes); + console.error(`📚 已加载 ${recipes.length} 个菜谱`); + } catch (error) { + console.error('加载菜谱数据失败:', error); + recipes = []; + categories = []; + throw error; + } +} + +// 启动服务的主函数 +async function main() { + // 加载菜谱数据 + await loadRecipeData(); + + if (TRANSPORT_TYPE === "http" || TRANSPORT_TYPE === "sse") { + const httpServer = createServer(async (req, res) => { + const url = new URL(req.url || "", `http://${req.headers.host}`).pathname; + + // 设置 CORS 头 + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, MCP-Session-Id, mcp-session-id"); + + // 处理预检请求 + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + try { + // 为每个请求创建新的服务器实例 + const requestServer = createServerInstance(); + + if (url === "/mcp") { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await requestServer.connect(transport); + await transport.handleRequest(req, res); + }else if (url === "/sse" && req.method === "GET") { + // Create new SSE transport for GET request + const sseTransport = new SSEServerTransport("/messages", res); + // Store the transport by session ID + sseTransports[sseTransport.sessionId] = sseTransport; + // Clean up transport when connection closes + res.on("close", () => { + delete sseTransports[sseTransport.sessionId]; + }); + await requestServer.connect(sseTransport); + } else if (url === "/messages" && req.method === "POST") { + // Get session ID from query parameters + const sessionId = + new URL(req.url || "", `http://${req.headers.host}`).searchParams.get("sessionId") ?? + ""; + + if (!sessionId) { + res.writeHead(400); + res.end("Missing sessionId parameter"); + return; + } + + // Get existing transport for this session + const sseTransport = sseTransports[sessionId]; + if (!sseTransport) { + res.writeHead(400); + res.end(`No transport found for sessionId: ${sessionId}`); + return; + } + + // Handle the POST message with the existing transport + await sseTransport.handlePostMessage(req, res); + } + else if (url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok", transport: TRANSPORT_TYPE })); + } else if (url === "/info") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + name: "HowToCook MCP Server", + version: "0.1.1", + transport: TRANSPORT_TYPE, + endpoints: { + mcp: "/mcp", + sse: "/sse", + health: "/health", + info: "/info" + }, + recipeCount: recipes.length + })); + } else { + res.writeHead(404); + res.end("Not found"); + } + } catch (error) { + console.error("处理请求时出错:", error); + if (!res.headersSent) { + res.writeHead(500); + res.end("Internal Server Error"); + } + } + }); + + httpServer.listen(PORT, () => { + console.error(`🚀 HowToCook MCP ${TRANSPORT_TYPE.toUpperCase()} 服务器启动成功`); + if(TRANSPORT_TYPE === "http"){ + console.error(`🔗 MCP 端点: http://localhost:${PORT}/mcp`); + }else if(TRANSPORT_TYPE === "sse"){ + console.error(`🔗 MCP 端点: http://localhost:${PORT}/sse`); + } + console.error(`💡 健康检查: http://localhost:${PORT}/health`); + console.error(`ℹ️ 服务器信息: http://localhost:${PORT}/info`); + }); + } else { + // stdio 模式 + const server = createServerInstance(); + const transport = new StdioServerTransport(); + try { + await server.connect(transport); + console.error('HowToCook MCP STDIO 服务器启动成功'); + } catch (error) { + console.error('服务器启动失败:', error); + process.exit(1); + } + } +} + +// 优雅关闭 +process.on('SIGINT', async () => { + console.error('\n正在关闭服务器...'); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.error('\n收到终止信号,正在关闭服务器...'); + process.exit(0); +}); + +// 启动服务器 +main().catch((error) => { + console.error('启动服务器失败:', error); + process.exit(1); +}); + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getAllRecipes.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getAllRecipes.ts new file mode 100644 index 00000000..3842ed3e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getAllRecipes.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; +import { Recipe } from "../types/index.js"; +import { simplifyRecipeNameOnly } from "../utils/recipeUtils.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export function registerGetAllRecipesTool(server: McpServer, recipes: Recipe[]) { + server.tool( + "mcp_howtocook_getAllRecipes", + "获取所有菜谱", + { + 'no_param': z.string().optional() + .describe('无参数') + }, + async () => { + // 返回更简化版的菜谱数据,只包含name和description + const simplifiedRecipes = recipes.map(simplifyRecipeNameOnly); + return { + content: [ + { + type: "text", + text: JSON.stringify(simplifiedRecipes, null, 2), + }, + ], + }; + } + ); +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipeById.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipeById.ts new file mode 100644 index 00000000..ee106c12 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipeById.ts @@ -0,0 +1,80 @@ +import { z } from "zod"; +import { Recipe } from "../types/index.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export function registerGetRecipeByIdTool(server: McpServer, recipes: Recipe[]) { + server.tool( + "mcp_howtocook_getRecipeById", + "根据菜谱名称或ID查询指定菜谱的完整详情,包括食材、步骤等", + { + query: z.string().describe('菜谱名称或ID,支持模糊匹配菜谱名称') + }, + async ({ query }: { query: string }) => { + // 首先尝试精确匹配ID + let foundRecipe = recipes.find(recipe => recipe.id === query); + + // 如果没有找到,尝试精确匹配名称 + if (!foundRecipe) { + foundRecipe = recipes.find(recipe => recipe.name === query); + } + + // 如果还没有找到,尝试模糊匹配名称 + if (!foundRecipe) { + foundRecipe = recipes.find(recipe => + recipe.name.toLowerCase().includes(query.toLowerCase()) + ); + } + + // 如果仍然没有找到,返回所有可能的匹配项(最多5个) + if (!foundRecipe) { + const possibleMatches = recipes.filter(recipe => + recipe.name.toLowerCase().includes(query.toLowerCase()) || + recipe.description.toLowerCase().includes(query.toLowerCase()) + ).slice(0, 5); + + if (possibleMatches.length === 0) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: "未找到匹配的菜谱", + query: query, + suggestion: "请检查菜谱名称是否正确,或尝试使用关键词搜索" + }, null, 2), + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + message: "未找到精确匹配,以下是可能的匹配项:", + query: query, + possibleMatches: possibleMatches.map(recipe => ({ + id: recipe.id, + name: recipe.name, + description: recipe.description, + category: recipe.category + })) + }, null, 2), + }, + ], + }; + } + + // 返回找到的完整菜谱信息 + return { + content: [ + { + type: "text", + text: JSON.stringify(foundRecipe, null, 2), + }, + ], + }; + } + ); +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipesByCategory.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipesByCategory.ts new file mode 100644 index 00000000..88ac8c85 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipesByCategory.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { Recipe } from "../types/index.js"; +import { simplifyRecipe } from "../utils/recipeUtils.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export function registerGetRecipesByCategoryTool(server: McpServer, recipes: Recipe[], categories: string[]) { + server.tool( + "mcp_howtocook_getRecipesByCategory", + `根据分类查询菜谱,可选分类有: ${categories.join(', ')}`, + { + category: z.enum(categories as [string, ...string[]]) + .describe('菜谱分类名称,如水产、早餐、荤菜、主食等') + }, + async ({ category }: { category: string }) => { + const filteredRecipes = recipes.filter((recipe) => recipe.category === category); + // 返回简化版的菜谱数据 + const simplifiedRecipes = filteredRecipes.map(simplifyRecipe); + return { + content: [ + { + type: "text", + text: JSON.stringify(simplifiedRecipes, null, 2), + }, + ], + }; + } + ); +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/recommendMeals.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/recommendMeals.ts new file mode 100644 index 00000000..c65a4a62 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/recommendMeals.ts @@ -0,0 +1,235 @@ +import { z } from "zod"; +import { Recipe, MealPlan, SimpleRecipe, DayPlan } from "../types/index.js"; +import { simplifyRecipe, processRecipeIngredients, categorizeIngredients } from "../utils/recipeUtils.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export function registerRecommendMealsTool(server: McpServer, recipes: Recipe[]) { + server.tool( + "mcp_howtocook_recommendMeals", + "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单", + { + allergies: z.array(z.string()).optional() + .describe('过敏原列表,如["大蒜", "虾"]'), + avoidItems: z.array(z.string()).optional() + .describe('忌口食材列表,如["葱", "姜"]'), + peopleCount: z.number().int().min(1).max(10) + .describe('用餐人数,1-10之间的整数') + }, + async ({ allergies = [], avoidItems = [], peopleCount }: { + allergies?: string[], + avoidItems?: string[], + peopleCount: number + }) => { + // 过滤掉含有忌口和过敏原的菜谱 + const filteredRecipes = recipes.filter((recipe) => { + // 检查是否包含过敏原或忌口食材 + const hasAllergiesOrAvoidItems = recipe.ingredients?.some((ingredient) => { + const name = ingredient.name?.toLowerCase() || ''; + return allergies.some(allergy => name.includes(allergy.toLowerCase())) || + avoidItems.some(item => name.includes(item.toLowerCase())); + }); + + return !hasAllergiesOrAvoidItems; + }); + + // 将菜谱按分类分组 + const recipesByCategory: Record = {}; + const targetCategories = ['水产', '早餐', '荤菜', '主食']; + + filteredRecipes.forEach((recipe) => { + if (targetCategories.includes(recipe.category)) { + if (!recipesByCategory[recipe.category]) { + recipesByCategory[recipe.category] = []; + } + recipesByCategory[recipe.category].push(recipe); + } + }); + + // 创建每周膳食计划 + const mealPlan: MealPlan = { + weekdays: [], + weekend: [], + groceryList: { + ingredients: [], + shoppingPlan: { + fresh: [], + pantry: [], + spices: [], + others: [] + } + } + }; + + // 用于跟踪已经选择的菜谱,以便后续处理食材信息 + const selectedRecipes: Recipe[] = []; + + // 周一至周五 + for (let i = 0; i < 5; i++) { + const dayPlan: DayPlan = { + day: ['周一', '周二', '周三', '周四', '周五'][i], + breakfast: [], + lunch: [], + dinner: [] + }; + + // 早餐 - 根据人数推荐1-2个早餐菜单 + const breakfastCount = Math.max(1, Math.ceil(peopleCount / 5)); + for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) { + const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length); + const selectedRecipe = recipesByCategory['早餐'][breakfastIndex]; + selectedRecipes.push(selectedRecipe); + dayPlan.breakfast.push(simplifyRecipe(selectedRecipe)); + // 避免重复,从候选列表中移除 + recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex); + } + + // 午餐和晚餐的菜谱数量,根据人数确定 + const mealCount = Math.max(2, Math.ceil(peopleCount / 3)); + + // 午餐 + for (let j = 0; j < mealCount; j++) { + // 随机选择菜系:主食、水产、蔬菜、荤菜等 + const categories = ['主食', '水产', '荤菜', '素菜', '甜品']; + let selectedCategory = categories[Math.floor(Math.random() * categories.length)]; + + // 如果该分类没有菜谱或已用完,尝试其他分类 + while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) { + selectedCategory = categories[Math.floor(Math.random() * categories.length)]; + if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) { + break; // 所有分类都没有可用菜谱,退出循环 + } + } + + if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) { + const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length); + const selectedRecipe = recipesByCategory[selectedCategory][index]; + selectedRecipes.push(selectedRecipe); + dayPlan.lunch.push(simplifyRecipe(selectedRecipe)); + // 避免重复,从候选列表中移除 + recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index); + } + } + + // 晚餐 + for (let j = 0; j < mealCount; j++) { + // 随机选择菜系,与午餐类似但可添加汤羹 + const categories = ['主食', '水产', '荤菜', '素菜', '甜品', '汤羹']; + let selectedCategory = categories[Math.floor(Math.random() * categories.length)]; + + // 如果该分类没有菜谱或已用完,尝试其他分类 + while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) { + selectedCategory = categories[Math.floor(Math.random() * categories.length)]; + if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) { + break; // 所有分类都没有可用菜谱,退出循环 + } + } + + if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) { + const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length); + const selectedRecipe = recipesByCategory[selectedCategory][index]; + selectedRecipes.push(selectedRecipe); + dayPlan.dinner.push(simplifyRecipe(selectedRecipe)); + // 避免重复,从候选列表中移除 + recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index); + } + } + + mealPlan.weekdays.push(dayPlan); + } + + // 周六和周日 + for (let i = 0; i < 2; i++) { + const dayPlan: DayPlan = { + day: ['周六', '周日'][i], + breakfast: [], + lunch: [], + dinner: [] + }; + + // 早餐 - 根据人数推荐菜品,至少2个菜品,随人数增加 + const breakfastCount = Math.max(2, Math.ceil(peopleCount / 3)); + for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) { + const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length); + const selectedRecipe = recipesByCategory['早餐'][breakfastIndex]; + selectedRecipes.push(selectedRecipe); + dayPlan.breakfast.push(simplifyRecipe(selectedRecipe)); + recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex); + } + + // 计算工作日的基础菜品数量 + const weekdayMealCount = Math.max(2, Math.ceil(peopleCount / 3)); + // 周末菜品数量:比工作日多1-2个菜,随人数增加 + const weekendAddition = peopleCount <= 4 ? 1 : 2; // 4人以下多1个菜,4人以上多2个菜 + const mealCount = weekdayMealCount + weekendAddition; + + const getMeals = (count: number): SimpleRecipe[] => { + const result: SimpleRecipe[] = []; + const categories = ['荤菜', '水产']; + + // 尽量平均分配不同分类的菜品 + for (let j = 0; j < count; j++) { + const category = categories[j % categories.length]; + if (recipesByCategory[category] && recipesByCategory[category].length > 0) { + const index = Math.floor(Math.random() * recipesByCategory[category].length); + const selectedRecipe = recipesByCategory[category][index]; + selectedRecipes.push(selectedRecipe); + result.push(simplifyRecipe(selectedRecipe)); + recipesByCategory[category] = recipesByCategory[category].filter((_, idx) => idx !== index); + } else if (recipesByCategory['主食'] && recipesByCategory['主食'].length > 0) { + // 如果没有足够的荤菜或水产,使用主食 + const index = Math.floor(Math.random() * recipesByCategory['主食'].length); + const selectedRecipe = recipesByCategory['主食'][index]; + selectedRecipes.push(selectedRecipe); + result.push(simplifyRecipe(selectedRecipe)); + recipesByCategory['主食'] = recipesByCategory['主食'].filter((_, idx) => idx !== index); + } + } + + return result; + }; + + dayPlan.lunch = getMeals(mealCount); + dayPlan.dinner = getMeals(mealCount); + + mealPlan.weekend.push(dayPlan); + } + + // 统计食材清单,收集所有菜谱的所有食材 + const ingredientMap = new Map(); + + // 处理所有菜谱 + selectedRecipes.forEach(recipe => processRecipeIngredients(recipe, ingredientMap)); + + // 整理食材清单 + for (const [name, info] of ingredientMap.entries()) { + mealPlan.groceryList.ingredients.push({ + name, + totalQuantity: info.totalQuantity, + unit: info.unit, + recipeCount: info.recipeCount, + recipes: info.recipes + }); + } + + // 对食材按使用频率排序 + mealPlan.groceryList.ingredients.sort((a, b) => b.recipeCount - a.recipeCount); + + // 生成购物计划,根据食材类型进行分类 + categorizeIngredients(mealPlan.groceryList.ingredients, mealPlan.groceryList.shoppingPlan); + + return { + content: [ + { + type: "text", + text: JSON.stringify(mealPlan, null, 2), + }, + ], + }; + } + ); +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/whatToEat.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/whatToEat.ts new file mode 100644 index 00000000..0ff8f38f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/whatToEat.ts @@ -0,0 +1,113 @@ +import { z } from "zod"; +import { Recipe, DishRecommendation } from "../types/index.js"; +import { simplifyRecipe } from "../utils/recipeUtils.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export function registerWhatToEatTool(server: McpServer, recipes: Recipe[]) { + server.tool( + "mcp_howtocook_whatToEat", + "不知道吃什么?根据人数直接推荐适合的菜品组合", + { + peopleCount: z.number().int().min(1).max(10) + .describe('用餐人数,1-10之间的整数,会根据人数推荐合适数量的菜品') + }, + async ({ peopleCount }: { peopleCount: number }) => { + // 根据人数计算荤素菜数量 + const vegetableCount = Math.floor((peopleCount + 1) / 2); + const meatCount = Math.ceil((peopleCount + 1) / 2); + + // 获取所有荤菜 + let meatDishes = recipes.filter((recipe) => + recipe.category === '荤菜' || recipe.category === '水产' + ); + + // 获取其他可能的菜品(当做素菜) + let vegetableDishes = recipes.filter((recipe) => + recipe.category !== '荤菜' && recipe.category !== '水产' && + recipe.category !== '早餐' && recipe.category !== '主食' + ); + + // 特别处理:如果人数超过8人,增加鱼类荤菜 + let recommendedDishes: Recipe[] = []; + let fishDish: Recipe | null = null; + + if (peopleCount > 8) { + const fishDishes = recipes.filter((recipe) => recipe.category === '水产'); + if (fishDishes.length > 0) { + fishDish = fishDishes[Math.floor(Math.random() * fishDishes.length)]; + recommendedDishes.push(fishDish); + } + } + + // 打乱肉类优先级顺序,增加随机性 + const meatTypes = ['猪肉', '鸡肉', '牛肉', '羊肉', '鸭肉', '鱼肉']; + // 使用 Fisher-Yates 洗牌算法打乱数组 + for (let i = meatTypes.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [meatTypes[i], meatTypes[j]] = [meatTypes[j], meatTypes[i]]; + } + + const selectedMeatDishes: Recipe[] = []; + + // 需要选择的荤菜数量 + const remainingMeatCount = fishDish ? meatCount - 1 : meatCount; + + // 尝试按照随机化的肉类优先级选择荤菜 + for (const meatType of meatTypes) { + if (selectedMeatDishes.length >= remainingMeatCount) break; + + const meatTypeOptions = meatDishes.filter((dish) => { + // 检查菜品的材料是否包含这种肉类 + return dish.ingredients?.some((ingredient) => { + const name = ingredient.name?.toLowerCase() || ''; + return name.includes(meatType.toLowerCase()); + }); + }); + + if (meatTypeOptions.length > 0) { + // 随机选择一道这种肉类的菜 + const selected = meatTypeOptions[Math.floor(Math.random() * meatTypeOptions.length)]; + selectedMeatDishes.push(selected); + // 从可选列表中移除,避免重复选择 + meatDishes = meatDishes.filter((dish) => dish.id !== selected.id); + } + } + + // 如果通过肉类筛选的荤菜不够,随机选择剩余的 + while (selectedMeatDishes.length < remainingMeatCount && meatDishes.length > 0) { + const randomIndex = Math.floor(Math.random() * meatDishes.length); + selectedMeatDishes.push(meatDishes[randomIndex]); + meatDishes.splice(randomIndex, 1); + } + + // 随机选择素菜 + const selectedVegetableDishes: Recipe[] = []; + while (selectedVegetableDishes.length < vegetableCount && vegetableDishes.length > 0) { + const randomIndex = Math.floor(Math.random() * vegetableDishes.length); + selectedVegetableDishes.push(vegetableDishes[randomIndex]); + vegetableDishes.splice(randomIndex, 1); + } + + // 合并推荐菜单 + recommendedDishes = recommendedDishes.concat(selectedMeatDishes, selectedVegetableDishes); + + // 构建推荐结果 + const recommendationDetails: DishRecommendation = { + peopleCount, + meatDishCount: selectedMeatDishes.length + (fishDish ? 1 : 0), + vegetableDishCount: selectedVegetableDishes.length, + dishes: recommendedDishes.map(simplifyRecipe), + message: `为${peopleCount}人推荐的菜品,包含${selectedMeatDishes.length + (fishDish ? 1 : 0)}个荤菜和${selectedVegetableDishes.length}个素菜。` + }; + + return { + content: [ + { + type: "text", + text: JSON.stringify(recommendationDetails, null, 2), + }, + ], + }; + } + ); +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/types/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/types/index.ts new file mode 100644 index 00000000..decbc89f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/types/index.ts @@ -0,0 +1,87 @@ +// 定义菜谱的类型接口 +export interface Ingredient { + name: string; + quantity: number | null; + unit: string | null; + text_quantity: string; + notes: string; +} + +export interface Step { + step: number; + description: string; +} + +export interface Recipe { + id: string; + name: string; + description: string; + source_path: string; + image_path: string | null; + category: string; + difficulty: number; + tags: string[]; + servings: number; + ingredients: Ingredient[]; + steps: Step[]; + prep_time_minutes: number | null; + cook_time_minutes: number | null; + total_time_minutes: number | null; + additional_notes: string[]; +} + +// 添加简化版的Recipe接口,只包含id、name和description +export interface SimpleRecipe { + id: string; + name: string; + description: string; + ingredients: { + name: string; + text_quantity: string; + }[]; +} + +// 更简化的Recipe接口,只包含name和description,用于getAllRecipes +export interface NameOnlyRecipe { + name: string; + description: string; +} + +// 定义膳食计划相关接口 +export interface MealPlan { + weekdays: Array; + weekend: Array; + groceryList: GroceryList; +} + +export interface DayPlan { + day: string; + breakfast: SimpleRecipe[]; + lunch: SimpleRecipe[]; + dinner: SimpleRecipe[]; +} + +export interface GroceryList { + ingredients: Array<{ + name: string; + totalQuantity: number | null; + unit: string | null; + recipeCount: number; + recipes: string[]; + }>; + shoppingPlan: { + fresh: string[]; + pantry: string[]; + spices: string[]; + others: string[]; + }; +} + +// 定义推荐菜品的接口 +export interface DishRecommendation { + peopleCount: number; + meatDishCount: number; + vegetableDishCount: number; + dishes: SimpleRecipe[]; + message: string; +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/utils/recipeUtils.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/utils/recipeUtils.ts new file mode 100644 index 00000000..bef44e69 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/utils/recipeUtils.ts @@ -0,0 +1,91 @@ +import { Recipe, SimpleRecipe, NameOnlyRecipe, Ingredient } from '../types/index.js'; + +// 创建简化版的Recipe数据 +export function simplifyRecipe(recipe: Recipe): SimpleRecipe { + return { + id: recipe.id, + name: recipe.name, + description: recipe.description, + ingredients: recipe.ingredients.map((ingredient: Ingredient) => ({ + name: ingredient.name, + text_quantity: ingredient.text_quantity + })) + }; +} + +// 创建只包含name和description的Recipe数据 +export function simplifyRecipeNameOnly(recipe: Recipe): NameOnlyRecipe { + return { + name: recipe.name, + description: recipe.description + }; +} + +// 处理食材清单,收集菜谱的所有食材 +export function processRecipeIngredients(recipe: Recipe, ingredientMap: Map) { + recipe.ingredients?.forEach((ingredient: Ingredient) => { + const key = ingredient.name.toLowerCase(); + + if (!ingredientMap.has(key)) { + ingredientMap.set(key, { + totalQuantity: ingredient.quantity, + unit: ingredient.unit, + recipeCount: 1, + recipes: [recipe.name] + }); + } else { + const existing = ingredientMap.get(key)!; + + // 对于有明确数量和单位的食材,进行汇总 + if (existing.unit && ingredient.unit && existing.unit === ingredient.unit && existing.totalQuantity !== null && ingredient.quantity !== null) { + existing.totalQuantity += ingredient.quantity; + } else { + // 否则保留 null,表示数量不确定 + existing.totalQuantity = null; + existing.unit = null; + } + + existing.recipeCount += 1; + if (!existing.recipes.includes(recipe.name)) { + existing.recipes.push(recipe.name); + } + } + }); +} + +// 根据食材类型进行分类 +export function categorizeIngredients(ingredients: Array<{ + name: string, + totalQuantity: number | null, + unit: string | null, + recipeCount: number, + recipes: string[] +}>, shoppingPlan: { + fresh: string[], + pantry: string[], + spices: string[], + others: string[] +}) { + const spiceKeywords = ['盐', '糖', '酱油', '醋', '料酒', '香料', '胡椒', '孜然', '辣椒', '花椒', '姜', '蒜', '葱', '调味']; + const freshKeywords = ['肉', '鱼', '虾', '蛋', '奶', '菜', '菠菜', '白菜', '青菜', '豆腐', '生菜', '水产', '豆芽', '西红柿', '番茄', '水果', '香菇', '木耳', '蘑菇']; + const pantryKeywords = ['米', '面', '粉', '油', '酒', '醋', '糖', '盐', '酱', '豆', '干', '罐头', '方便面', '面条', '米饭', '意大利面', '燕麦']; + + ingredients.forEach(ingredient => { + const name = ingredient.name.toLowerCase(); + + if (spiceKeywords.some(keyword => name.includes(keyword))) { + shoppingPlan.spices.push(ingredient.name); + } else if (freshKeywords.some(keyword => name.includes(keyword))) { + shoppingPlan.fresh.push(ingredient.name); + } else if (pantryKeywords.some(keyword => name.includes(keyword))) { + shoppingPlan.pantry.push(ingredient.name); + } else { + shoppingPlan.others.push(ingredient.name); + } + }); +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/tsconfig.json new file mode 100644 index 00000000..d03b0e00 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.github/workflows/publish.yml new file mode 100644 index 00000000..f98890ff --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Publish to PyPI + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Version to publish (leave empty to use pyproject.toml version)' + required: false + type: string + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.gitignore new file mode 100644 index 00000000..88b7cb8b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.gitignore @@ -0,0 +1,14 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +*.bak +test/ +.DS_Store +CLAUDE.md \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/Dockerfile new file mode 100644 index 00000000..87fc3705 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/Dockerfile @@ -0,0 +1,19 @@ +# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile +FROM python:3.10-alpine + +# Install system dependencies (if any required, e.g., for pillow) +RUN apk add --no-cache gcc musl-dev libffi-dev + +# Set work directory +WORKDIR /app + +# Copy the application code +COPY . . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Expose port if needed (not needed for stdio) + +# Set the entrypoint to run the MCP server +ENTRYPOINT ["python", "ppt_mcp_server.py"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/LICENSE new file mode 100644 index 00000000..31323d1d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 GongRzhe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/README.md new file mode 100644 index 00000000..a6e104a1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/README.md @@ -0,0 +1,986 @@ +# Office-PowerPoint-MCP-Server +[![smithery badge](https://smithery.ai/badge/@GongRzhe/Office-PowerPoint-MCP-Server)](https://smithery.ai/server/@GongRzhe/Office-PowerPoint-MCP-Server) +![](https://badge.mcpx.dev?type=server 'MCP Server') + +A comprehensive MCP (Model Context Protocol) server for PowerPoint manipulation using python-pptx. **Version 2.0** provides 32 powerful tools organized into 11 specialized modules, offering complete PowerPoint creation, management, and professional design capabilities. The server features a modular architecture with enhanced parameter handling, intelligent operation selection, and comprehensive error handling. + +---- + +# **Not so ugly anymore with new slide_layout_templates** + +截屏2025-06-20 15 53 45 + +---- + +### Example + +#### Prompt + +650f4cc5d0f1ea4f3b1580800cb0deb + +#### Output + +084f1cf4bc7e4fcd4890c8f94f536c1 + +#### Demo's GIF -> (./public/demo.mp4) + +![demo](./public/demo.gif) + +## Features + +### Core PowerPoint Operations +- **Round-trip support** for any Open XML presentation (.pptx file) including all elements +- **Template support** with automatic theme and layout preservation +- **Multi-presentation management** with global state tracking +- **Core document properties** management (title, subject, author, keywords, comments) + +### Content Creation & Management +- **Slide management** with flexible layout selection +- **Text manipulation** with placeholder population and bullet point creation +- **Advanced text formatting** with font, color, alignment, and style controls +- **Text validation** with automatic fit checking and optimization suggestions + +### Visual Elements +- **Image handling** with file and base64 input support +- **Image enhancement** using Pillow with brightness, contrast, saturation, and filter controls +- **Professional image effects** including shadows, reflections, glows, and soft edges +- **Shape creation** with 20+ auto shape types (rectangles, ovals, flowchart elements, etc.) +- **Table creation** with advanced cell formatting and styling + +### Charts & Data Visualization +- **Chart support** for column, bar, line, and pie charts +- **Data series management** with categories and multiple series support +- **Chart formatting** with legends, data labels, and titles + +### Professional Design Features +- **4 professional color schemes** (Modern Blue, Corporate Gray, Elegant Green, Warm Red) +- **Professional typography** with Segoe UI font family and size presets +- **Theme application** with automatic styling across presentations +- **Gradient backgrounds** with customizable directions and color schemes +- **Slide enhancement** tools for existing content +- **25 built-in slide templates** with dynamic sizing and visual effects +- **Advanced template features** including auto-wrapping, dynamic font sizing, and professional animations + +### Advanced Features +- **Font analysis and optimization** using FontTools +- **Picture effects** with 9 different visual effects (shadow, reflection, glow, bevel, etc.) +- **Comprehensive validation** with automatic error fixing +- **Template search** with configurable directory paths +- **Professional layout calculations** with margin and spacing management + +## Installation + +### Installing via Smithery + +To install PowerPoint Manipulation Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@GongRzhe/Office-PowerPoint-MCP-Server): + +```bash +npx -y @smithery/cli install @GongRzhe/Office-PowerPoint-MCP-Server --client claude +``` + +### Prerequisites + +- Python 3.6 or higher (as specified in pyproject.toml) +- pip package manager +- Optional: uvx for package execution without local installation + +### Installation Options + +#### Option 1: Using the Setup Script (Recommended) + +The easiest way to set up the PowerPoint MCP Server is using the provided setup script, which automates the installation process: + +```bash +python setup_mcp.py +``` + +This script will: +- Check prerequisites +- Offer installation options: + - Install from PyPI (recommended for most users) + - Set up local development environment +- Install required dependencies +- Generate the appropriate MCP configuration file +- Provide instructions for integrating with Claude Desktop + +The script offers different paths based on your environment: +- If you have `uvx` installed, it will configure using UVX (recommended) +- If the server is already installed, it provides configuration options +- If the server is not installed, it offers installation methods + +#### Option 2: Manual Installation + +1. Clone the repository: + ```bash + git clone https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git + cd Office-PowerPoint-MCP-Server + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Make the server executable: + ```bash + chmod +x ppt_mcp_server.py + ``` + +## Usage + +Display help text: +```bash +python ppt_mcp_server.py -h +``` + +### Starting the Stdio Server + +Run the stdio server: + +```bash +python ppt_mcp_server.py +``` + +### Starting the Streamable-Http Server + +Run the streamable-http server on port 8000: + +```bash +python ppt_mcp_server.py --transport http --port 8000 +``` + +Run in Docker +```bash +docker build -t ppt_mcp_server . +docker run -d --rm -p 8000:8000 ppt_mcp_server -t http +``` + + +### MCP Configuration + +#### Option 1: Local Python Server + +Add the server to your MCP settings configuration file: + +```json +{ + "mcpServers": { + "ppt": { + "command": "python", + "args": ["/path/to/ppt_mcp_server.py"], + "env": {} + } + } +} +``` + +#### Option 2: Using UVX (No Local Installation Required) + +If you have `uvx` installed, you can run the server directly from PyPI without local installation: + +```json +{ + "mcpServers": { + "ppt": { + "command": "uvx", + "args": [ + "--from", "office-powerpoint-mcp-server", "ppt_mcp_server" + ], + "env": {} + } + } +} +``` + +## 🚀 What's New in v2.0 + +### **Comprehensive Tool Suite (32 Tools)** +- **Complete PowerPoint manipulation** with 34 specialized tools +- **11 organized modules** covering all aspects of presentation creation +- **Enhanced parameter handling** with comprehensive validation +- **Intelligent defaults** and operation-based interfaces + +### **Built-in Slide Templates** +- **25+ professional slide templates** with dynamic features built-in +- **Advanced template system** with auto-generation capabilities +- **Auto-sizing text** that adapts to content length and container size +- **Professional visual effects** including shadows, glows, and gradients +- **Complete presentation generation** from template sequences + +### **Modular Architecture** +- **11 specialized modules**: presentation, content, structural, professional, template, hyperlink, chart, connector, master, and transition tools +- **Better maintainability** with separated concerns +- **Easier extensibility** for adding new features +- **Cleaner code structure** with shared utilities + +## Available Tools + +The server provides **34 specialized tools** organized into the following categories: + +### **Presentation Management (7 tools)** +1. **create_presentation** - Create new presentations +2. **create_presentation_from_template** - Create from templates with theme preservation +3. **open_presentation** - Open existing presentations +4. **save_presentation** - Save presentations to files +5. **get_presentation_info** - Get comprehensive presentation information +6. **get_template_file_info** - Analyze template files and layouts +7. **set_core_properties** - Set document properties + +### **Content Management (8 tools)** +8. **add_slide** - Add slides with optional background styling +9. **get_slide_info** - Get detailed slide information +10. **extract_slide_text** - ✨ **NEW** Extract all text content from a specific slide +11. **extract_presentation_text** - ✨ **NEW** Extract text content from all slides in presentation +12. **populate_placeholder** - Populate placeholders with text +13. **add_bullet_points** - Add formatted bullet points +14. **manage_text** - ✨ **Unified text tool** (add/format/validate/format_runs) +15. **manage_image** - ✨ **Unified image tool** (add/enhance) + +### **Template Operations (7 tools)** +16. **list_slide_templates** - Browse available slide layout templates +17. **apply_slide_template** - Apply structured layout templates to existing slides +18. **create_slide_from_template** - Create new slides using layout templates +19. **create_presentation_from_templates** - Create complete presentations from template sequences +20. **get_template_info** - Get detailed information about specific templates +21. **auto_generate_presentation** - Automatically generate presentations based on topic +22. **optimize_slide_text** - Optimize text elements for better readability and fit + +### **Structural Elements (4 tools)** +23. **add_table** - Create tables with enhanced formatting +24. **format_table_cell** - Format individual table cells +25. **add_shape** - Add shapes with text and formatting options +26. **add_chart** - Create charts with comprehensive customization + +### **Professional Design (3 tools)** +27. **apply_professional_design** - ✨ **Unified design tool** (themes/slides/enhancement) +28. **apply_picture_effects** - ✨ **Unified effects tool** (9+ effects combined) +29. **manage_fonts** - ✨ **Unified font tool** (analyze/optimize/recommend) + +### **Specialized Features (5 tools)** +30. **manage_hyperlinks** - Complete hyperlink management (add/remove/list/update) +31. **manage_slide_masters** - Access and manage slide master properties and layouts +32. **add_connector** - Add connector lines/arrows between points on slides +33. **update_chart_data** - Replace existing chart data with new categories and series +34. **manage_slide_transitions** - Basic slide transition management + +## 🌟 Key Unified Tools + +### **`manage_text`** - All-in-One Text Management +```python +# Add text box +manage_text(slide_index=0, operation="add", text="Hello World", font_size=24) + +# Format existing text +manage_text(slide_index=0, operation="format", shape_index=0, bold=True, color=[255,0,0]) + +# Validate text fit with auto-fix +manage_text(slide_index=0, operation="validate", shape_index=0, validation_only=False) +``` + +### **`manage_image`** - Complete Image Handling +```python +# Add image with enhancement +manage_image(slide_index=0, operation="add", image_source="logo.png", + enhancement_style="presentation") + +# Enhance existing image +manage_image(slide_index=0, operation="enhance", image_source="photo.jpg", + brightness=1.2, contrast=1.1, saturation=1.3) +``` + +### **`apply_picture_effects`** - Multiple Effects in One Call +```python +# Apply combined effects +apply_picture_effects(slide_index=0, shape_index=0, effects={ + "shadow": {"blur_radius": 4.0, "color": [128,128,128]}, + "glow": {"size": 5.0, "color": [0,176,240]}, + "rotation": {"rotation": 15.0} +}) +``` + +### **`apply_professional_design`** - Theme & Design Management +```python +# Add professional slide +apply_professional_design(operation="slide", slide_type="title_content", + color_scheme="modern_blue", title="My Presentation") + +# Apply theme to entire presentation +apply_professional_design(operation="theme", color_scheme="corporate_gray") + +# Enhance existing slide +apply_professional_design(operation="enhance", slide_index=0, color_scheme="elegant_green") +``` + +## Examples + +### Creating a New Presentation + +```python +# Create a new presentation +result = use_mcp_tool( + server_name="ppt", + tool_name="create_presentation", + arguments={} +) +presentation_id = result["presentation_id"] + +# Add a title slide +result = use_mcp_tool( + server_name="ppt", + tool_name="add_slide", + arguments={ + "layout_index": 0, # Title slide layout + "title": "My Presentation", + "presentation_id": presentation_id + } +) +slide_index = result["slide_index"] + +# Populate subtitle placeholder +result = use_mcp_tool( + server_name="ppt", + tool_name="populate_placeholder", + arguments={ + "slide_index": slide_index, + "placeholder_idx": 1, # Subtitle placeholder + "text": "Created with PowerPoint MCP Server", + "presentation_id": presentation_id + } +) + +# Save the presentation +result = use_mcp_tool( + server_name="ppt", + tool_name="save_presentation", + arguments={ + "file_path": "my_presentation.pptx", + "presentation_id": presentation_id + } +) +``` + +### Creating a Professional Presentation with v2.0 + +```python +# Create a professional slide with modern styling - CONSOLIDATED TOOL +result = use_mcp_tool( + server_name="ppt", + tool_name="apply_professional_design", + arguments={ + "operation": "slide", + "slide_type": "title_content", + "color_scheme": "modern_blue", + "title": "Quarterly Business Review", + "content": [ + "Revenue increased by 15% compared to last quarter", + "Customer satisfaction scores reached all-time high of 94%", + "Successfully launched 3 new product features", + "Expanded team by 12 new talented professionals" + ] + } +) + +# Apply professional theme to entire presentation - SAME TOOL, DIFFERENT OPERATION +result = use_mcp_tool( + server_name="ppt", + tool_name="apply_professional_design", + arguments={ + "operation": "theme", + "color_scheme": "modern_blue", + "apply_to_existing": True + } +) + +# Add slide with gradient background - ENHANCED ADD_SLIDE +result = use_mcp_tool( + server_name="ppt", + tool_name="add_slide", + arguments={ + "layout_index": 0, + "background_type": "professional_gradient", + "color_scheme": "modern_blue", + "gradient_direction": "diagonal" + } +) +``` + +### Working with Built-in Slide Templates (New in v2.0) + +```python +# List all available slide templates with their features +result = use_mcp_tool( + server_name="ppt", + tool_name="list_slide_templates", + arguments={} +) + +# Apply a professional template to an existing slide +result = use_mcp_tool( + server_name="ppt", + tool_name="apply_slide_template", + arguments={ + "slide_index": 0, + "template_id": "title_slide", + "color_scheme": "modern_blue", + "content_mapping": { + "title": "Quarterly Business Review", + "subtitle": "Q4 2024 Results", + "author": "Leadership Team" + } + } +) + +# Create a new slide using a template +result = use_mcp_tool( + server_name="ppt", + tool_name="create_slide_from_template", + arguments={ + "template_id": "text_with_image", + "color_scheme": "elegant_green", + "content_mapping": { + "title": "Our Revolutionary Solution", + "content": "• 250% increase in efficiency\n• 98% customer satisfaction\n• Industry-leading performance" + }, + "image_paths": { + "supporting": "path/to/product_image.jpg" + } + } +) + +# Generate a complete presentation from multiple templates +result = use_mcp_tool( + server_name="ppt", + tool_name="create_presentation_from_templates", + arguments={ + "template_sequence": [ + { + "template_id": "title_slide", + "content": { + "title": "2024 Annual Report", + "subtitle": "Growth and Innovation", + "author": "Executive Team" + } + }, + { + "template_id": "key_metrics_dashboard", + "content": { + "metric_1_value": "94%", + "metric_2_value": "$2.4M", + "metric_3_value": "247" + } + }, + { + "template_id": "before_after_comparison", + "content": { + "content_left": "Manual processes taking hours", + "content_right": "Automated workflows in minutes" + } + } + ], + "color_scheme": "modern_blue" + } +) +``` + +### Enhanced Image Management with v2.0 + +```python +# Add image with automatic enhancement - CONSOLIDATED TOOL +result = use_mcp_tool( + server_name="ppt", + tool_name="manage_image", + arguments={ + "slide_index": 1, + "operation": "add", + "image_source": "company_logo.png", + "left": 1.0, + "top": 1.0, + "width": 3.0, + "height": 2.0, + "enhancement_style": "presentation" + } +) + +# Apply multiple picture effects at once - CONSOLIDATED TOOL +result = use_mcp_tool( + server_name="ppt", + tool_name="apply_picture_effects", + arguments={ + "slide_index": 1, + "shape_index": 0, + "effects": { + "shadow": { + "shadow_type": "outer", + "blur_radius": 4.0, + "distance": 3.0, + "direction": 315.0, + "color": [128, 128, 128], + "transparency": 0.6 + }, + "glow": { + "size": 5.0, + "color": [0, 176, 240], + "transparency": 0.4 + } + } + } +) +``` + +### Advanced Text Management with v2.0 + +```python +# Add and format text in one operation - CONSOLIDATED TOOL +result = use_mcp_tool( + server_name="ppt", + tool_name="manage_text", + arguments={ + "slide_index": 0, + "operation": "add", + "left": 1.0, + "top": 2.0, + "width": 8.0, + "height": 1.5, + "text": "Welcome to Our Quarterly Review", + "font_size": 32, + "font_name": "Segoe UI", + "bold": True, + "color": [0, 120, 215], + "alignment": "center", + "auto_fit": True + } +) + +# Validate and fix text fit issues - SAME TOOL, DIFFERENT OPERATION +result = use_mcp_tool( + server_name="ppt", + tool_name="manage_text", + arguments={ + "slide_index": 0, + "operation": "validate", + "shape_index": 0, + "validation_only": False, # Auto-fix enabled + "min_font_size": 10, + "max_font_size": 48 + } +) +``` + +### Creating a Presentation from Template + +```python +# First, inspect a template to see its layouts and properties +result = use_mcp_tool( + server_name="ppt", + tool_name="get_template_info", + arguments={ + "template_path": "company_template.pptx" + } +) +template_info = result + +# Create a new presentation from the template +result = use_mcp_tool( + server_name="ppt", + tool_name="create_presentation_from_template", + arguments={ + "template_path": "company_template.pptx" + } +) +presentation_id = result["presentation_id"] + +# Add a slide using one of the template's layouts +result = use_mcp_tool( + server_name="ppt", + tool_name="add_slide", + arguments={ + "layout_index": 1, # Use layout from template + "title": "Quarterly Report", + "presentation_id": presentation_id + } +) + +# Save the presentation +result = use_mcp_tool( + server_name="ppt", + tool_name="save_presentation", + arguments={ + "file_path": "quarterly_report.pptx", + "presentation_id": presentation_id + } +) +``` + +### Adding Advanced Charts and Data Visualization + +```python +# Add a chart slide +result = use_mcp_tool( + server_name="ppt", + tool_name="add_slide", + arguments={ + "layout_index": 1, # Content slide layout + "title": "Sales Data", + "presentation_id": presentation_id + } +) +slide_index = result["slide_index"] + +# Add a column chart with comprehensive customization +result = use_mcp_tool( + server_name="ppt", + tool_name="add_chart", + arguments={ + "slide_index": slide_index, + "chart_type": "column", + "left": 1.0, + "top": 2.0, + "width": 8.0, + "height": 4.5, + "categories": ["Q1", "Q2", "Q3", "Q4"], + "series_names": ["2023", "2024"], + "series_values": [ + [100, 120, 140, 160], + [110, 130, 150, 170] + ], + "has_legend": True, + "legend_position": "bottom", + "has_data_labels": True, + "title": "Quarterly Sales Performance", + "presentation_id": presentation_id + } +) +``` + +### Text Validation and Optimization with v2.0 + +```python +# Validate text fit and get optimization suggestions - USING CONSOLIDATED TOOL +result = use_mcp_tool( + server_name="ppt", + tool_name="manage_text", + arguments={ + "slide_index": 0, + "operation": "validate", + "shape_index": 0, + "text": "This is a very long title that might not fit properly in the designated text box area", + "font_size": 24, + "validation_only": True + } +) + +# Comprehensive slide validation with automatic fixes - SAME TOOL, AUTO-FIX ENABLED +result = use_mcp_tool( + server_name="ppt", + tool_name="manage_text", + arguments={ + "slide_index": 0, + "operation": "validate", + "shape_index": 0, + "validation_only": False, # Auto-fix enabled + "min_font_size": 10, + "max_font_size": 48 + } +) +``` + +### Reading Slide Content with New Text Extraction Tools (v2.1) + +```python +# Extract text content from a specific slide - NEW TOOL +result = use_mcp_tool( + server_name="ppt", + tool_name="extract_slide_text", + arguments={ + "slide_index": 0, + "presentation_id": presentation_id + } +) + +# The result includes: +{ + "success": True, + "slide_index": 0, + "text_content": { + "slide_title": "Quarterly Business Review", + "placeholders": [ + { + "shape_index": 1, + "shape_name": "Subtitle Placeholder 2", + "text": "Q4 2024 Results", + "placeholder_type": "SUBTITLE", + "placeholder_idx": 1 + } + ], + "text_shapes": [ + { + "shape_index": 3, + "shape_name": "TextBox 4", + "text": "Revenue increased by 15%" + } + ], + "table_text": [], + "all_text_combined": "Quarterly Business Review\nQ4 2024 Results\nRevenue increased by 15%" + }, + "total_text_shapes": 2, + "has_title": True, + "has_tables": False +} + +# Extract text from all slides in the presentation - NEW TOOL +result = use_mcp_tool( + server_name="ppt", + tool_name="extract_presentation_text", + arguments={ + "presentation_id": presentation_id, + "include_slide_info": True + } +) + +# The result includes comprehensive text extraction: +{ + "success": True, + "presentation_id": "pres_123", + "total_slides": 5, + "slides_with_text": 4, + "total_text_shapes": 12, + "slides_with_titles": 3, + "slides_with_tables": 1, + "slides_text": [...], # Detailed per-slide text content + "all_presentation_text_combined": "=== SLIDE 1 ===\nTitle Here\nContent here..." +} + +# Extract text without additional slide metadata for cleaner output +result = use_mcp_tool( + server_name="ppt", + tool_name="extract_presentation_text", + arguments={ + "presentation_id": presentation_id, + "include_slide_info": False + } +) +``` + +## Template Support + +### Working with Templates + +The PowerPoint MCP Server provides comprehensive template support for creating presentations from existing template files. This feature enables: + +- **Corporate branding** with predefined themes, layouts, and styles +- **Consistent presentations** across teams and projects +- **Custom slide masters** and specialized layouts +- **Pre-configured properties** and document settings +- **Flexible template discovery** with configurable search paths + +### Template File Requirements + +- **Supported formats**: `.pptx` and `.potx` files +- **Existing content**: Templates can contain existing slides (preserved during creation) +- **Layout availability**: All custom layouts and slide masters are accessible +- **Search locations**: Configurable via `PPT_TEMPLATE_PATH` environment variable +- **Default search paths**: Current directory, `./templates`, `./assets`, `./resources` + +### Template Configuration + +Set the `PPT_TEMPLATE_PATH` environment variable to specify custom template directories: + +```bash +# Unix/Linux/macOS +export PPT_TEMPLATE_PATH="/path/to/templates:/another/path" + +# Windows +set PPT_TEMPLATE_PATH="C:\templates;C:\company_templates" +``` + +### Template Workflow + +1. **Inspect Template**: Use `get_template_info` to analyze available layouts and properties +2. **Create from Template**: Use `create_presentation_from_template` with automatic theme preservation +3. **Use Template Layouts**: Reference layout indices from template analysis when adding slides +4. **Maintain Branding**: Template themes, fonts, and colors are automatically applied to new content + +### Professional Color Schemes + +The server includes 4 built-in professional color schemes: +- **Modern Blue**: Microsoft-inspired blue theme with complementary colors +- **Corporate Gray**: Professional grayscale theme with blue accents +- **Elegant Green**: Forest green theme with cream and light green accents +- **Warm Red**: Deep red theme with orange and yellow accents + +Each scheme includes primary, secondary, accent, light, and text colors optimized for business presentations. + +## 🎨 Built-in Slide Templates (New in v2.0) + +The PowerPoint MCP Server now includes **25 professional slide templates** with advanced dynamic features. All templates support: + +### **Dynamic Features** +- **Automatic text sizing** based on content length and container dimensions +- **Intelligent text wrapping** to fit within specified areas +- **Visual effects** including shadows, glows, and outlines +- **Gradient backgrounds** with multi-layer compositions +- **Professional animations** ready for presentation delivery +- **Interactive hover effects** for enhanced user experience +- **Smart content overflow handling** with automatic adjustments + +### **Available Template Categories** + +#### **Title & Introduction Slides** +- `title_slide` - Dynamic title slide with gradient background and text effects +- `chapter_intro` - Section divider with chapter numbering and styling +- `thank_you_slide` - Closing slide with contact information and effects + +#### **Content Layout Slides** +- `text_with_image` - Text content with stylized image and interactive elements +- `two_column_text` - Two equal columns of text with dynamic sizing +- `two_column_text_images` - Two columns with text and corresponding images +- `three_column_layout` - Three equal columns with text and images +- `full_image_slide` - Large background image with text overlay + +#### **Business & Analytics Slides** +- `key_metrics_dashboard` - Interactive metrics dashboard with animated counters +- `before_after_comparison` - Dynamic comparison layout with visual dividers +- `chart_comparison` - Two charts side by side for performance comparison +- `data_table_slide` - Slide focused on tabular data with professional styling +- `timeline_slide` - Horizontal timeline with milestones and effects + +#### **Process & Flow Slides** +- `process_flow` - Step-by-step process visualization with enhanced effects +- `agenda_slide` - Table of contents or agenda overview with styling +- `quote_testimonial` - Featured quote or customer testimonial with effects + +#### **Team & Organization Slides** +- `team_introduction` - Team member showcase with photos and roles + +### **Template Usage Examples** + +```python +# Browse all available templates +templates = use_mcp_tool("ppt", "list_slide_templates", {}) + +# Key templates with their features: +{ + "title_slide": { + "features": ["Dynamic text sizing", "Gradient backgrounds", "Text effects"], + "elements": ["title", "subtitle", "author", "decorative_accent"] + }, + "key_metrics_dashboard": { + "features": ["Animated counters", "Gradient containers", "Trend visualization"], + "elements": ["3 metric containers", "trend chart", "insights callout"] + }, + "before_after_comparison": { + "features": ["Split gradient background", "VS divider", "Improvement arrow"], + "elements": ["before/after headers", "comparison content", "improvement metrics"] + } +} +``` + +### **Color Scheme Integration** +All templates work seamlessly with the 4 professional color schemes: +- **modern_blue**: Microsoft-inspired theme with dynamic gradients +- **corporate_gray**: Professional grayscale with blue accents +- **elegant_green**: Forest green with cream and light accents +- **warm_red**: Deep red with orange and yellow highlights + +### **Dynamic Content Adaptation** +Templates automatically adjust to content: +- **Font sizes** scale based on text length (8pt - 44pt range) +- **Line spacing** adjusts for readability (1.0x - 1.4x) +- **Text wrapping** intelligently breaks lines at optimal points +- **Container sizing** adapts to content overflow +- **Visual effects** scale appropriately with element sizes + +## 📁 File Structure + +``` +Office-PowerPoint-MCP-Server/ +├── ppt_mcp_server.py # Main consolidated server (v2.0) +├── slide_layout_templates.json # 25+ professional slide templates with dynamic features +├── tools/ # 11 specialized tool modules (32 tools total) +│ ├── __init__.py +│ ├── presentation_tools.py # Presentation management (7 tools) +│ ├── content_tools.py # Content & slides (6 tools) +│ ├── template_tools.py # Template operations (7 tools) +│ ├── structural_tools.py # Tables, shapes, charts (4 tools) +│ ├── professional_tools.py # Themes, effects, fonts (3 tools) +│ ├── hyperlink_tools.py # Hyperlink management (1 tool) +│ ├── chart_tools.py # Advanced chart operations (1 tool) +│ ├── connector_tools.py # Connector lines/arrows (1 tool) +│ ├── master_tools.py # Slide master management (1 tool) +│ └── transition_tools.py # Slide transitions (1 tool) +├── utils/ # 7 organized utility modules (68+ functions) +│ ├── __init__.py +│ ├── core_utils.py # Error handling & safe operations +│ ├── presentation_utils.py # Presentation management utilities +│ ├── content_utils.py # Content & slide operations +│ ├── design_utils.py # Themes, colors, effects & fonts +│ ├── template_utils.py # Template management & dynamic features +│ └── validation_utils.py # Text & layout validation +├── setup_mcp.py # Interactive setup script +├── pyproject.toml # Updated for v2.0 +└── README.md # This documentation +``` + +## 🏗️ Architecture Benefits + +### **Modular Design** +- **7 focused utility modules** with clear responsibilities +- **11 organized tool modules** for comprehensive coverage +- **68+ utility functions** organized by functionality +- **32 MCP tools** covering all PowerPoint manipulation needs +- **Clear separation of concerns** for easier development + +### **Code Organization** +- **Logical grouping** of related functionality across modules +- **Better discoverability** with organized tool categories +- **Improved testability** with isolated modules +- **Future extensibility** through modular structure + +### **Comprehensive Coverage** +- **Complete PowerPoint lifecycle** from creation to presentation +- **Advanced template system** with auto-generation capabilities +- **Professional design tools** with multiple effects and styling options +- **Specialized features** including hyperlinks, connectors, and slide masters + +### **Developer Experience** +- **Clear responsibility boundaries** between modules +- **Easier debugging** with smaller, focused files +- **Simpler testing** with isolated functionality +- **Enhanced maintainability** through separation of concerns + +## 🔄 What's New in Version 2.0 + +**Enhanced functionality with comprehensive tool coverage!** The updated server provides: + +### **New Specialized Tools Added:** +- **`manage_hyperlinks`** - Complete hyperlink management for text elements +- **`update_chart_data`** - Advanced chart data replacement and updating +- **`add_connector`** - Connector lines and arrows between slide elements +- **`manage_slide_masters`** - Access to slide master properties and layouts +- **`manage_slide_transitions`** - Basic slide transition management +- **`auto_generate_presentation`** - AI-powered presentation generation +- **`optimize_slide_text`** - Text optimization for better readability + +### **Enhanced Existing Tools:** +- **`manage_text`** - Now supports text run formatting with `format_runs` operation +- **`create_presentation_from_templates`** - Enhanced template sequence processing +- **`apply_picture_effects`** - Expanded effect combinations and options + +## 🔄 What's New in Version 2.1 + +**Text extraction capabilities added!** Now you can read content from existing presentations: + +### **New Text Extraction Tools Added:** +- **`extract_slide_text`** - Extract all text content from a specific slide including titles, placeholders, text shapes, and tables +- **`extract_presentation_text`** - Extract text content from all slides in a presentation with comprehensive statistics and combined output + +### **Key Features of Text Extraction:** +- **Complete text coverage** - Extracts from titles, placeholders, text boxes, and table cells +- **Structured output** - Organized by content type (titles, placeholders, shapes, tables) +- **Presentation-wide analysis** - Statistics on text distribution across slides +- **Flexible output options** - Individual slide content or combined presentation text +- **Error handling** - Graceful handling of slides that cannot be processed + +## License + +MIT diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/__init__.py new file mode 100644 index 00000000..085298ed --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/__init__.py @@ -0,0 +1 @@ +# PowerPoint MCP Server \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp-config.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp-config.json new file mode 100644 index 00000000..102b316d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp-config.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "ppt": { + "command": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/.venv/bin/python", + "args": [ + "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/ppt_mcp_server.py" + ], + "env": { + "PYTHONPATH": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server", + "PPT_TEMPLATE_PATH": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/templates" + } + } + } +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_all_tools_templates_effects_demo.pptx b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_all_tools_templates_effects_demo.pptx new file mode 100644 index 00000000..521f92df Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_all_tools_templates_effects_demo.pptx differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_config_sample.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_config_sample.json new file mode 100644 index 00000000..59b7652e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_config_sample.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "word-document-server": { + "command": "D:\\BackDataService\\Office-Word-MCP-Server\\.venv\\Scripts\\python.exe", + "args": [ + "D:\\BackDataService\\Office-Word-MCP-Server\\word_server.py" + ], + "env": { + "PYTHONPATH": "D:\\BackDataService\\Office-Word-MCP-Server" + } + } + } +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/ppt_mcp_server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/ppt_mcp_server.py new file mode 100644 index 00000000..cad383bd --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/ppt_mcp_server.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python +""" +MCP Server for PowerPoint manipulation using python-pptx. +Consolidated version with 20 tools organized into multiple modules. +""" +import os +import argparse +from typing import Dict, Any +from mcp.server.fastmcp import FastMCP + +# import utils # Currently unused +from tools import ( + register_presentation_tools, + register_content_tools, + register_structural_tools, + register_professional_tools, + register_template_tools, + register_hyperlink_tools, + register_chart_tools, + register_connector_tools, + register_master_tools, + register_transition_tools +) + +# Initialize the FastMCP server +app = FastMCP( + name="ppt-mcp-server" +) + +# Global state to store presentations in memory +presentations = {} +current_presentation_id = None + +# Template configuration +def get_template_search_directories(): + """ + Get list of directories to search for templates. + Uses environment variable PPT_TEMPLATE_PATH if set, otherwise uses default directories. + + Returns: + List of directories to search for templates + """ + template_env_path = os.environ.get('PPT_TEMPLATE_PATH') + + if template_env_path: + # If environment variable is set, use it as the primary template directory + # Support multiple paths separated by colon (Unix) or semicolon (Windows) + import platform + separator = ';' if platform.system() == "Windows" else ':' + env_dirs = [path.strip() for path in template_env_path.split(separator) if path.strip()] + + # Verify that the directories exist + valid_env_dirs = [] + for dir_path in env_dirs: + expanded_path = os.path.expanduser(dir_path) + if os.path.exists(expanded_path) and os.path.isdir(expanded_path): + valid_env_dirs.append(expanded_path) + + if valid_env_dirs: + # Add default fallback directories + return valid_env_dirs + ['.', './templates', './assets', './resources'] + else: + print(f"Warning: PPT_TEMPLATE_PATH directories not found: {template_env_path}") + + # Default search directories when no environment variable or invalid paths + return ['.', './templates', './assets', './resources'] + +# ---- Helper Functions ---- + +def get_current_presentation(): + """Get the current presentation object or raise an error if none is loaded.""" + if current_presentation_id is None or current_presentation_id not in presentations: + raise ValueError("No presentation is currently loaded. Please create or open a presentation first.") + return presentations[current_presentation_id] + +def get_current_presentation_id(): + """Get the current presentation ID.""" + return current_presentation_id + +def set_current_presentation_id(pres_id): + """Set the current presentation ID.""" + global current_presentation_id + current_presentation_id = pres_id + +def validate_parameters(params): + """ + Validate parameters against constraints. + + Args: + params: Dictionary of parameter name: (value, constraints) pairs + + Returns: + (True, None) if all valid, or (False, error_message) if invalid + """ + for param_name, (value, constraints) in params.items(): + for constraint_func, error_msg in constraints: + if not constraint_func(value): + return False, f"Parameter '{param_name}': {error_msg}" + return True, None + +def is_positive(value): + """Check if a value is positive.""" + return value > 0 + +def is_non_negative(value): + """Check if a value is non-negative.""" + return value >= 0 + +def is_in_range(min_val, max_val): + """Create a function that checks if a value is in a range.""" + return lambda x: min_val <= x <= max_val + +def is_in_list(valid_list): + """Create a function that checks if a value is in a list.""" + return lambda x: x in valid_list + +def is_valid_rgb(color_list): + """Check if a color list is a valid RGB tuple.""" + if not isinstance(color_list, list) or len(color_list) != 3: + return False + return all(isinstance(c, int) and 0 <= c <= 255 for c in color_list) + +def add_shape_direct(slide, shape_type: str, left: float, top: float, width: float, height: float) -> Any: + """ + Add an auto shape to a slide using direct integer values instead of enum objects. + + This implementation provides a reliable alternative that bypasses potential + enum-related issues in the python-pptx library. + + Args: + slide: The slide object + shape_type: Shape type string (e.g., 'rectangle', 'oval', 'triangle') + left: Left position in inches + top: Top position in inches + width: Width in inches + height: Height in inches + + Returns: + The created shape + """ + from pptx.util import Inches + + # Direct mapping of shape types to their integer values + # Values from MSO_AUTO_SHAPE_TYPE enum: https://github.com/scanny/python-pptx/blob/master/src/pptx/enum/shapes.py + shape_type_map = { + 'rectangle': 1, # RECTANGLE + 'rounded_rectangle': 5, # ROUNDED_RECTANGLE + 'oval': 9, # OVAL + 'diamond': 4, # DIAMOND + 'triangle': 7, # ISOSCELES_TRIANGLE + 'right_triangle': 8, # RIGHT_TRIANGLE + 'pentagon': 51, # PENTAGON + 'hexagon': 10, # HEXAGON + 'heptagon': 145, # HEPTAGON + 'octagon': 6, # OCTAGON + 'star': 92, # STAR_5_POINT + 'arrow': 33, # RIGHT_ARROW + 'cloud': 179, # CLOUD + 'heart': 21, # HEART + 'lightning_bolt': 22, # LIGHTNING_BOLT + 'sun': 23, # SUN + 'moon': 24, # MOON + 'smiley_face': 17, # SMILEY_FACE + 'no_symbol': 19, # NO_SYMBOL + 'flowchart_process': 61, # FLOWCHART_PROCESS + 'flowchart_decision': 63, # FLOWCHART_DECISION + 'flowchart_data': 64, # FLOWCHART_DATA + 'flowchart_document': 67 # FLOWCHART_DOCUMENT + } + + # Check if shape type is valid before trying to use it + shape_type_lower = str(shape_type).lower() + if shape_type_lower not in shape_type_map: + available_shapes = ', '.join(sorted(shape_type_map.keys())) + raise ValueError(f"Unsupported shape type: '{shape_type}'. Available shape types: {available_shapes}") + + # Get the integer value for the shape type + shape_value = shape_type_map[shape_type_lower] + + # Create the shape using the direct integer value + try: + # The integer value is passed directly to add_shape + shape = slide.shapes.add_shape( + shape_value, Inches(left), Inches(top), Inches(width), Inches(height) + ) + return shape + except Exception as e: + raise ValueError(f"Failed to create '{shape_type}' shape using direct value {shape_value}: {str(e)}") + +# ---- Custom presentation management wrapper ---- + +class PresentationManager: + """Wrapper to handle presentation state updates.""" + + def __init__(self, presentations_dict): + self.presentations = presentations_dict + + def store_presentation(self, pres, pres_id): + """Store a presentation and set it as current.""" + self.presentations[pres_id] = pres + set_current_presentation_id(pres_id) + return pres_id + +# ---- Register Tools ---- + +# Create presentation manager wrapper +presentation_manager = PresentationManager(presentations) + +# Wrapper functions to handle state management +def create_presentation_wrapper(original_func): + """Wrapper to handle presentation creation with state management.""" + def wrapper(*args, **kwargs): + result = original_func(*args, **kwargs) + if "presentation_id" in result and result["presentation_id"] in presentations: + set_current_presentation_id(result["presentation_id"]) + return result + return wrapper + +def open_presentation_wrapper(original_func): + """Wrapper to handle presentation opening with state management.""" + def wrapper(*args, **kwargs): + result = original_func(*args, **kwargs) + if "presentation_id" in result and result["presentation_id"] in presentations: + set_current_presentation_id(result["presentation_id"]) + return result + return wrapper + +# Register all tool modules +register_presentation_tools( + app, + presentations, + get_current_presentation_id, + get_template_search_directories +) + +register_content_tools( + app, + presentations, + get_current_presentation_id, + validate_parameters, + is_positive, + is_non_negative, + is_in_range, + is_valid_rgb +) + +register_structural_tools( + app, + presentations, + get_current_presentation_id, + validate_parameters, + is_positive, + is_non_negative, + is_in_range, + is_valid_rgb, + add_shape_direct +) + +register_professional_tools( + app, + presentations, + get_current_presentation_id +) + +register_template_tools( + app, + presentations, + get_current_presentation_id +) + +register_hyperlink_tools( + app, + presentations, + get_current_presentation_id, + validate_parameters, + is_positive, + is_non_negative, + is_in_range, + is_valid_rgb +) + +register_chart_tools( + app, + presentations, + get_current_presentation_id, + validate_parameters, + is_positive, + is_non_negative, + is_in_range, + is_valid_rgb +) + + +register_connector_tools( + app, + presentations, + get_current_presentation_id, + validate_parameters, + is_positive, + is_non_negative, + is_in_range, + is_valid_rgb +) + +register_master_tools( + app, + presentations, + get_current_presentation_id, + validate_parameters, + is_positive, + is_non_negative, + is_in_range, + is_valid_rgb +) + +register_transition_tools( + app, + presentations, + get_current_presentation_id, + validate_parameters, + is_positive, + is_non_negative, + is_in_range, + is_valid_rgb +) + + +# ---- Additional Utility Tools ---- + +@app.tool() +def list_presentations() -> Dict: + """List all loaded presentations.""" + return { + "presentations": [ + { + "id": pres_id, + "slide_count": len(pres.slides), + "is_current": pres_id == current_presentation_id + } + for pres_id, pres in presentations.items() + ], + "current_presentation_id": current_presentation_id, + "total_presentations": len(presentations) + } + +@app.tool() +def switch_presentation(presentation_id: str) -> Dict: + """Switch to a different loaded presentation.""" + if presentation_id not in presentations: + return { + "error": f"Presentation '{presentation_id}' not found. Available presentations: {list(presentations.keys())}" + } + + global current_presentation_id + old_id = current_presentation_id + current_presentation_id = presentation_id + + return { + "message": f"Switched from presentation '{old_id}' to '{presentation_id}'", + "previous_presentation_id": old_id, + "current_presentation_id": current_presentation_id + } + +@app.tool() +def get_server_info() -> Dict: + """Get information about the MCP server.""" + return { + "name": "PowerPoint MCP Server - Enhanced Edition", + "version": "2.1.0", + "total_tools": 32, # Organized into 11 specialized modules + "loaded_presentations": len(presentations), + "current_presentation": current_presentation_id, + "features": [ + "Presentation Management (7 tools)", + "Content Management (6 tools)", + "Template Operations (7 tools)", + "Structural Elements (4 tools)", + "Professional Design (3 tools)", + "Specialized Features (5 tools)" + ], + "improvements": [ + "32 specialized tools organized into 11 focused modules", + "68+ utility functions across 7 organized utility modules", + "Enhanced parameter handling and validation", + "Unified operation interfaces with comprehensive coverage", + "Advanced template system with auto-generation capabilities", + "Professional design tools with multiple effects and styling", + "Specialized features including hyperlinks, connectors, slide masters", + "Dynamic text sizing and intelligent wrapping", + "Advanced visual effects and styling", + "Content-aware optimization and validation", + "Complete PowerPoint lifecycle management", + "Modular architecture for better maintainability" + ], + "new_enhanced_features": [ + "Hyperlink Management - Add, update, remove, and list hyperlinks in text", + "Advanced Chart Data Updates - Replace chart data with new categories and series", + "Advanced Text Run Formatting - Apply formatting to specific text runs", + "Shape Connectors - Add connector lines and arrows between points", + "Slide Master Management - Access and manage slide masters and layouts", + "Slide Transitions - Basic transition management (placeholder for future)" + ] + } + +# ---- Main Function ---- +def main(transport: str = "stdio", port: int = 8000): + if transport == "http": + import asyncio + # Set the port for HTTP transport + app.settings.port = port + # Start the FastMCP server with HTTP transport + try: + app.run(transport='streamable-http') + except asyncio.exceptions.CancelledError: + print("Server stopped by user.") + except KeyboardInterrupt: + print("Server stopped by user.") + except Exception as e: + print(f"Error starting server: {e}") + + elif transport == "sse": + # Run the FastMCP server in SSE (Server Side Events) mode + app.run(transport='sse') + + else: + # Run the FastMCP server + app.run(transport='stdio') + +if __name__ == "__main__": + # Parse command line arguments + parser = argparse.ArgumentParser(description="MCP Server for PowerPoint manipulation using python-pptx") + + parser.add_argument( + "-t", + "--transport", + type=str, + default="stdio", + choices=["stdio", "http", "sse"], + help="Transport method for the MCP server (default: stdio)" + ) + + parser.add_argument( + "-p", + "--port", + type=int, + default=8000, + help="Port to run the MCP server on (default: 8000)" + ) + args = parser.parse_args() + main(args.transport, args.port) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.gif b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.gif new file mode 100644 index 00000000..7795f49b Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.gif differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.mp4 b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.mp4 new file mode 100644 index 00000000..4188f2f8 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.mp4 differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/pyproject.toml new file mode 100644 index 00000000..f9a21a07 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "office-powerpoint-mcp-server" +version = "2.0.7" +description = "MCP Server for PowerPoint manipulation using python-pptx - Consolidated Edition" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + {name = "GongRzhe", email = "gongrzhe@gmail.com"} +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +requires-python = ">=3.10" +dependencies = [ + "python-pptx>=0.6.21", + "mcp[cli]>=1.8.0", + "Pillow>=8.0.0", + "fonttools>=4.0.0", +] + +[project.urls] +"Homepage" = "https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git" +"Bug Tracker" = "https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git/issues" + +[tool.hatch.build.targets.wheel] +only-include = ["ppt_mcp_server.py", "tools/", "utils/", "enhanced_slide_templates.json", "slide_layout_templates.json"] +sources = ["."] + +[tool.hatch.build] +exclude = [ + "public/demo.mp4", + "public/demo.gif", + "*.pptx" +] + +[project.scripts] +ppt_mcp_server = "ppt_mcp_server:main" \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/requirements.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/requirements.txt new file mode 100644 index 00000000..9f97c217 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/requirements.txt @@ -0,0 +1,4 @@ +mcp[cli] +python-pptx +Pillow +fonttools \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/setup_mcp.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/setup_mcp.py new file mode 100644 index 00000000..1f3343bc --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/setup_mcp.py @@ -0,0 +1,561 @@ +# Import necessary Python standard libraries +import os # For operating with file system, handling files and directory paths +import json # For processing JSON format data +import subprocess # For creating and managing subprocesses +import sys # For accessing Python interpreter related variables and functions +import platform # For getting current operating system information +import shutil # For checking if executables exist in PATH + +def check_prerequisites(): + """ + Check if necessary prerequisites are installed + + Returns: + tuple: (python_ok, uv_installed, uvx_installed, ppt_server_installed) + """ + # Check Python version + python_version = sys.version_info + python_ok = python_version.major >= 3 and python_version.minor >= 6 + + # Check if uv/uvx is installed + uv_installed = shutil.which("uv") is not None + uvx_installed = shutil.which("uvx") is not None + + # Check if office-powerpoint-mcp-server is already installed via pip + try: + result = subprocess.run( + [sys.executable, "-m", "pip", "show", "office-powerpoint-mcp-server"], + capture_output=True, + text=True, + check=False + ) + ppt_server_installed = result.returncode == 0 + except Exception: + ppt_server_installed = False + + return (python_ok, uv_installed, uvx_installed, ppt_server_installed) + +def setup_venv(): + """ + Function to set up Python virtual environment + + Features: + - Checks if Python version meets requirements (3.6+) + - Creates Python virtual environment (if it doesn't exist) + - Installs required dependencies in the newly created virtual environment + + No parameters required + + Returns: Path to Python interpreter in the virtual environment + """ + # Check Python version + python_version = sys.version_info + if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 6): + print("Error: Python 3.6 or higher is required.") + sys.exit(1) + + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + # Set virtual environment directory path + venv_path = os.path.join(base_path, '.venv') + + # Determine pip and python executable paths based on operating system + is_windows = platform.system() == "Windows" + if is_windows: + pip_path = os.path.join(venv_path, 'Scripts', 'pip.exe') + python_path = os.path.join(venv_path, 'Scripts', 'python.exe') + else: + pip_path = os.path.join(venv_path, 'bin', 'pip') + python_path = os.path.join(venv_path, 'bin', 'python') + + # Check if virtual environment already exists and is valid + venv_exists = os.path.exists(venv_path) + pip_exists = os.path.exists(pip_path) + + if not venv_exists or not pip_exists: + print("Creating new virtual environment...") + # Remove existing venv if it's invalid + if venv_exists and not pip_exists: + print("Existing virtual environment is incomplete, recreating it...") + try: + shutil.rmtree(venv_path) + except Exception as e: + print(f"Warning: Could not remove existing virtual environment: {e}") + print("Please delete the .venv directory manually and try again.") + sys.exit(1) + + # Create virtual environment + try: + subprocess.run([sys.executable, '-m', 'venv', venv_path], check=True) + print("Virtual environment created successfully!") + except subprocess.CalledProcessError as e: + print(f"Error creating virtual environment: {e}") + sys.exit(1) + else: + print("Valid virtual environment already exists.") + + # Double-check that pip exists after creating venv + if not os.path.exists(pip_path): + print(f"Error: pip executable not found at {pip_path}") + print("Try creating the virtual environment manually with: python -m venv .venv") + sys.exit(1) + + # Install or update dependencies + print("\nInstalling requirements...") + try: + # Install mcp package + subprocess.run([pip_path, 'install', 'mcp[cli]'], check=True) + # Install python-pptx package + subprocess.run([pip_path, 'install', 'python-pptx'], check=True) + + # Also install dependencies from requirements.txt if it exists + requirements_path = os.path.join(base_path, 'requirements.txt') + if os.path.exists(requirements_path): + subprocess.run([pip_path, 'install', '-r', requirements_path], check=True) + + + print("Requirements installed successfully!") + except subprocess.CalledProcessError as e: + print(f"Error installing requirements: {e}") + sys.exit(1) + except FileNotFoundError: + print(f"Error: Could not execute {pip_path}") + print("Try activating the virtual environment manually and installing requirements:") + if is_windows: + print(f".venv\\Scripts\\activate") + else: + print("source .venv/bin/activate") + print("pip install mcp[cli] python-pptx") + sys.exit(1) + + return python_path + +def generate_mcp_config_local(python_path): + """ + Generate MCP configuration for locally installed office-powerpoint-mcp-server + + Parameters: + - python_path: Path to Python interpreter in the virtual environment + + Returns: Path to the generated config file + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Path to PowerPoint Server script + server_script_path = os.path.join(base_path, 'ppt_mcp_server.py') + + # Path to templates directory + templates_path = os.path.join(base_path, 'templates') + + # Create MCP configuration dictionary + config = { + "mcpServers": { + "ppt": { + "command": python_path, + "args": [server_script_path], + "env": { + "PYTHONPATH": base_path, + "PPT_TEMPLATE_PATH": templates_path + } + } + } + } + + # Save configuration to JSON file + config_path = os.path.join(base_path, 'mcp-config.json') + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting + + return config_path + +def generate_mcp_config_uvx(): + """ + Generate MCP configuration for PyPI-installed office-powerpoint-mcp-server using UVX + + Returns: Path to the generated config file + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Path to templates directory (optional for UVX installs) + templates_path = os.path.join(base_path, 'templates') + + # Create MCP configuration dictionary + env_config = {} + if os.path.exists(templates_path): + env_config["PPT_TEMPLATE_PATH"] = templates_path + + config = { + "mcpServers": { + "ppt": { + "command": "uvx", + "args": ["--from", "office-powerpoint-mcp-server", "ppt_mcp_server"], + "env": env_config + } + } + } + + # Save configuration to JSON file + config_path = os.path.join(base_path, 'mcp-config.json') + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting + + return config_path + +def generate_mcp_config_module(): + """ + Generate MCP configuration for PyPI-installed office-powerpoint-mcp-server using Python module + + Returns: Path to the generated config file + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Path to templates directory (optional for module installs) + templates_path = os.path.join(base_path, 'templates') + + # Create MCP configuration dictionary + env_config = {} + if os.path.exists(templates_path): + env_config["PPT_TEMPLATE_PATH"] = templates_path + + config = { + "mcpServers": { + "ppt": { + "command": sys.executable, + "args": ["-m", "office_powerpoint_mcp_server"], + "env": env_config + } + } + } + + # Save configuration to JSON file + config_path = os.path.join(base_path, 'mcp-config.json') + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting + + return config_path + +def install_from_pypi(): + """ + Install office-powerpoint-mcp-server from PyPI + + Returns: True if successful, False otherwise + """ + print("\nInstalling office-powerpoint-mcp-server from PyPI...") + try: + subprocess.run([sys.executable, "-m", "pip", "install", "office-powerpoint-mcp-server"], check=True) + print("office-powerpoint-mcp-server successfully installed from PyPI!") + return True + except subprocess.CalledProcessError: + print("Failed to install office-powerpoint-mcp-server from PyPI.") + return False + +def print_config_instructions(config_path): + """ + Print instructions for using the generated config + + Parameters: + - config_path: Path to the generated config file + """ + print(f"\nMCP configuration has been written to: {config_path}") + + with open(config_path, 'r') as f: + config = json.load(f) + + print("\nMCP configuration for Claude Desktop:") + print(json.dumps(config, indent=2)) + + # Provide instructions for adding configuration to Claude Desktop configuration file + if platform.system() == "Windows": + claude_config_path = os.path.expandvars("%APPDATA%\\Claude\\claude_desktop_config.json") + else: # macOS + claude_config_path = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json") + + print(f"\nTo use with Claude Desktop, merge this configuration into: {claude_config_path}") + +def create_package_structure(): + """ + Create necessary package structure and directories + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Create __init__.py file + init_path = os.path.join(base_path, '__init__.py') + if not os.path.exists(init_path): + with open(init_path, 'w') as f: + f.write('# PowerPoint MCP Server') + print(f"Created __init__.py at: {init_path}") + + # Create requirements.txt file + requirements_path = os.path.join(base_path, 'requirements.txt') + if not os.path.exists(requirements_path): + with open(requirements_path, 'w') as f: + f.write('mcp[cli]\npython-pptx\n') + print(f"Created requirements.txt at: {requirements_path}") + + # Create templates directory for PowerPoint templates + templates_dir = os.path.join(base_path, 'templates') + if not os.path.exists(templates_dir): + os.makedirs(templates_dir) + print(f"Created templates directory at: {templates_dir}") + + # Create a README file in templates directory + readme_path = os.path.join(templates_dir, 'README.md') + with open(readme_path, 'w') as f: + f.write("""# PowerPoint Templates + +This directory is for storing PowerPoint template files (.pptx or .potx) that can be used with the MCP server. + +## Usage + +1. Place your template files in this directory +2. Use the `create_presentation_from_template` tool with the template filename +3. The server will automatically search for templates in this directory + +## Supported Formats + +- `.pptx` - PowerPoint presentation files +- `.potx` - PowerPoint template files + +## Example + +```python +# Create presentation from template +result = create_presentation_from_template("company_template.pptx") +``` + +The server will search for templates in: +- Current directory +- ./templates/ (this directory) +- ./assets/ +- ./resources/ +""") + print(f"Created templates README at: {readme_path}") + + # Offer to create a sample template + create_sample = input("\nWould you like to create a sample template for testing? (y/n): ").lower().strip() + if create_sample in ['y', 'yes']: + create_sample_template(templates_dir) + +def create_sample_template(templates_dir): + """ + Create a sample PowerPoint template for testing + + Parameters: + - templates_dir: Directory where templates are stored + """ + try: + # Import required modules for creating a sample template + from pptx import Presentation + from pptx.util import Inches, Pt + from pptx.dml.color import RGBColor + from pptx.enum.text import PP_ALIGN + + print("Creating sample template...") + + # Create a new presentation + prs = Presentation() + + # Get the title slide layout + title_slide_layout = prs.slide_layouts[0] + slide = prs.slides.add_slide(title_slide_layout) + + # Set title and subtitle + title = slide.shapes.title + subtitle = slide.placeholders[1] + + title.text = "Sample Company Template" + subtitle.text = "Professional Presentation Template\nCreated by PowerPoint MCP Server" + + # Format title + title_paragraph = title.text_frame.paragraphs[0] + title_paragraph.font.size = Pt(44) + title_paragraph.font.bold = True + title_paragraph.font.color.rgb = RGBColor(31, 73, 125) # Dark blue + + # Format subtitle + for paragraph in subtitle.text_frame.paragraphs: + paragraph.font.size = Pt(18) + paragraph.font.color.rgb = RGBColor(68, 84, 106) # Gray blue + paragraph.alignment = PP_ALIGN.CENTER + + # Add a content slide + content_slide_layout = prs.slide_layouts[1] + content_slide = prs.slides.add_slide(content_slide_layout) + + content_title = content_slide.shapes.title + content_title.text = "Sample Content Slide" + + # Add bullet points to content + content_placeholder = content_slide.placeholders[1] + text_frame = content_placeholder.text_frame + text_frame.text = "Key Features" + + # Add bullet points + bullet_points = [ + "Professional theme and colors", + "Custom layouts and placeholders", + "Ready for content creation", + "Compatible with MCP server tools" + ] + + for point in bullet_points: + p = text_frame.add_paragraph() + p.text = point + p.level = 1 + + # Add a section header slide + section_slide_layout = prs.slide_layouts[2] if len(prs.slide_layouts) > 2 else prs.slide_layouts[0] + section_slide = prs.slides.add_slide(section_slide_layout) + + if section_slide.shapes.title: + section_slide.shapes.title.text = "Template Features" + + # Save the sample template + template_path = os.path.join(templates_dir, 'sample_template.pptx') + prs.save(template_path) + + print(f"✅ Sample template created: {template_path}") + print(" You can now test the template feature with:") + print(" • get_template_info('sample_template.pptx')") + print(" • create_presentation_from_template('sample_template.pptx')") + + except ImportError: + print("⚠️ Cannot create sample template: python-pptx not installed yet") + print(" Run the setup first, then manually create templates in the templates/ directory") + except Exception as e: + print(f"❌ Failed to create sample template: {str(e)}") + print(" You can manually add template files to the templates/ directory") + +# Main execution entry point +if __name__ == '__main__': + # Check prerequisites + python_ok, uv_installed, uvx_installed, ppt_server_installed = check_prerequisites() + + if not python_ok: + print("Error: Python 3.6 or higher is required.") + sys.exit(1) + + print("PowerPoint MCP Server Setup") + print("===========================\n") + + # Create necessary files + create_package_structure() + + # If office-powerpoint-mcp-server is already installed, offer config options + if ppt_server_installed: + print("office-powerpoint-mcp-server is already installed via pip.") + + if uvx_installed: + print("\nOptions:") + print("1. Generate MCP config for UVX (recommended)") + print("2. Generate MCP config for Python module") + print("3. Set up local development environment") + + choice = input("\nEnter your choice (1-3): ") + + if choice == "1": + config_path = generate_mcp_config_uvx() + print_config_instructions(config_path) + elif choice == "2": + config_path = generate_mcp_config_module() + print_config_instructions(config_path) + elif choice == "3": + python_path = setup_venv() + config_path = generate_mcp_config_local(python_path) + print_config_instructions(config_path) + else: + print("Invalid choice. Exiting.") + sys.exit(1) + else: + print("\nOptions:") + print("1. Generate MCP config for Python module") + print("2. Set up local development environment") + + choice = input("\nEnter your choice (1-2): ") + + if choice == "1": + config_path = generate_mcp_config_module() + print_config_instructions(config_path) + elif choice == "2": + python_path = setup_venv() + config_path = generate_mcp_config_local(python_path) + print_config_instructions(config_path) + else: + print("Invalid choice. Exiting.") + sys.exit(1) + + # If office-powerpoint-mcp-server is not installed, offer installation options + else: + print("office-powerpoint-mcp-server is not installed.") + + print("\nOptions:") + print("1. Install from PyPI (recommended)") + print("2. Set up local development environment") + + choice = input("\nEnter your choice (1-2): ") + + if choice == "1": + if install_from_pypi(): + if uvx_installed: + print("\nNow generating MCP config for UVX...") + config_path = generate_mcp_config_uvx() + else: + print("\nUVX not found. Generating MCP config for Python module...") + config_path = generate_mcp_config_module() + print_config_instructions(config_path) + elif choice == "2": + python_path = setup_venv() + config_path = generate_mcp_config_local(python_path) + print_config_instructions(config_path) + else: + print("Invalid choice. Exiting.") + sys.exit(1) + + print("\nSetup complete! You can now use the PowerPoint MCP server with compatible clients like Claude Desktop.") + + print("\n" + "="*60) + print("POWERPOINT MCP SERVER - NEW FEATURES") + print("="*60) + print("\n📁 Template Support:") + print(" • Place PowerPoint templates (.pptx/.potx) in the ./templates/ directory") + print(" • Use 'create_presentation_from_template' tool to create presentations from templates") + print(" • Use 'get_template_info' tool to inspect template layouts and properties") + print(" • Templates preserve branding, themes, and custom layouts") + print(" • Template path configured via PPT_TEMPLATE_PATH environment variable") + + print("\n🔧 Available MCP Tools:") + print(" Presentations:") + print(" • create_presentation - Create new blank presentation") + print(" • create_presentation_from_template - Create from template file") + print(" • get_template_info - Inspect template file details") + print(" • open_presentation - Open existing presentation") + print(" • save_presentation - Save presentation to file") + + print("\n Content:") + print(" • add_slide - Add slides with various layouts") + print(" • add_textbox - Add formatted text boxes") + print(" • add_image - Add images from files or base64") + print(" • add_table - Add formatted tables") + print(" • add_shape - Add various auto shapes") + print(" • add_chart - Add column, bar, line, and pie charts") + + print("\n📚 Documentation:") + print(" • Full API documentation available in README.md") + print(" • Template usage examples included") + print(" • Check ./templates/README.md for template guidelines") + + print("\n🚀 Quick Start with Templates:") + print(" 1. Copy your .pptx template to ./templates/") + print(" 2. Use: create_presentation_from_template('your_template.pptx')") + print(" 3. Add slides using template layouts") + print(" 4. Save your presentation") + print("\n💡 Custom Template Paths:") + print(" • Set PPT_TEMPLATE_PATH environment variable for custom locations") + print(" • Supports multiple paths (colon-separated on Unix, semicolon on Windows)") + print(" • Example: PPT_TEMPLATE_PATH='/path/to/templates:/path/to/more/templates'") + + print("\n" + "="*60) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/slide_layout_templates.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/slide_layout_templates.json new file mode 100644 index 00000000..4f49c4e5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/slide_layout_templates.json @@ -0,0 +1,3690 @@ +{ + "metadata": { + "version": "4.1", + "description": "Unified comprehensive slide layout templates with enhanced dynamic features and modern 2024 design trends", + "created_date": "2025-01-20", + "updated_date": "2025-06-20", + "total_templates": 31, + "supported_features": [ + "Dynamic text sizing and wrapping", + "Multiple font combinations per template", + "Advanced visual effects for all elements", + "Gradient backgrounds and overlays", + "Shadow and glow effects", + "Automatic content adaptation", + "Stylistic text variations", + "Professional animation readiness", + "Interactive hover effects", + "Animated counters and metrics", + "Smart content overflow handling", + "Multi-layer gradient backgrounds", + "Text placeholders", + "Image placeholders", + "Chart placeholders", + "Table placeholders", + "Shape elements", + "Professional color schemes", + "Typography styles", + "Layout positioning", + "Custom image masks (circle, hexagon)", + "Interactive poll elements", + "Organic shape design", + "Neon cyberpunk styling", + "Nature-inspired themes", + "Minimalist clean aesthetics", + "Pastel color schemes", + "Brutalist bold typography", + "Split-screen comparisons", + "Product showcase layouts", + "Real-time visualization", + "Modern gradient effects" + ] + }, + "color_schemes": { + "modern_blue": { + "primary": [0, 120, 215], + "secondary": [40, 40, 40], + "accent1": [0, 176, 240], + "accent2": [255, 192, 0], + "light": [247, 247, 247], + "text": [68, 68, 68], + "gradient_start": [0, 176, 240], + "gradient_end": [0, 120, 215] + }, + "corporate_gray": { + "primary": [68, 68, 68], + "secondary": [0, 120, 215], + "accent1": [89, 89, 89], + "accent2": [217, 217, 217], + "light": [242, 242, 242], + "text": [51, 51, 51], + "gradient_start": [217, 217, 217], + "gradient_end": [89, 89, 89] + }, + "elegant_green": { + "primary": [70, 136, 71], + "secondary": [255, 255, 255], + "accent1": [146, 208, 80], + "accent2": [112, 173, 71], + "light": [238, 236, 225], + "text": [89, 89, 89], + "gradient_start": [146, 208, 80], + "gradient_end": [70, 136, 71] + }, + "warm_red": { + "primary": [192, 80, 77], + "secondary": [68, 68, 68], + "accent1": [230, 126, 34], + "accent2": [241, 196, 15], + "light": [253, 253, 253], + "text": [44, 62, 80], + "gradient_start": [241, 196, 15], + "gradient_end": [192, 80, 77] + }, + "pastel_dream": { + "primary": [156, 136, 255], + "secondary": [255, 154, 162], + "accent1": [255, 206, 147], + "accent2": [163, 228, 215], + "light": [248, 248, 255], + "text": [88, 88, 88], + "gradient_start": [255, 206, 147], + "gradient_end": [156, 136, 255] + }, + "nature_earth": { + "primary": [101, 123, 131], + "secondary": [162, 132, 94], + "accent1": [218, 215, 205], + "accent2": [143, 151, 121], + "light": [245, 243, 238], + "text": [68, 68, 68], + "gradient_start": [218, 215, 205], + "gradient_end": [101, 123, 131] + }, + "neon_vibrant": { + "primary": [255, 20, 147], + "secondary": [0, 191, 255], + "accent1": [57, 255, 20], + "accent2": [255, 140, 0], + "light": [248, 248, 248], + "text": [34, 34, 34], + "gradient_start": [255, 20, 147], + "gradient_end": [0, 191, 255] + }, + "minimalist_mono": { + "primary": [34, 34, 34], + "secondary": [128, 128, 128], + "accent1": [68, 68, 68], + "accent2": [187, 187, 187], + "light": [250, 250, 250], + "text": [51, 51, 51], + "gradient_start": [187, 187, 187], + "gradient_end": [68, 68, 68] + } + }, + "typography_styles": { + "modern_sans": { + "title": {"name": "Segoe UI", "weight": "bold", "style": "normal"}, + "subtitle": {"name": "Segoe UI Light", "weight": "normal", "style": "normal"}, + "body": {"name": "Segoe UI", "weight": "normal", "style": "normal"}, + "accent": {"name": "Segoe UI Semibold", "weight": "semibold", "style": "normal"} + }, + "elegant_serif": { + "title": {"name": "Times New Roman", "weight": "bold", "style": "normal"}, + "subtitle": {"name": "Georgia", "weight": "normal", "style": "italic"}, + "body": {"name": "Times New Roman", "weight": "normal", "style": "normal"}, + "accent": {"name": "Georgia", "weight": "bold", "style": "normal"} + }, + "tech_modern": { + "title": {"name": "Arial", "weight": "bold", "style": "normal"}, + "subtitle": {"name": "Helvetica", "weight": "light", "style": "normal"}, + "body": {"name": "Arial", "weight": "normal", "style": "normal"}, + "accent": {"name": "Helvetica", "weight": "bold", "style": "normal"} + }, + "organic_flow": { + "title": {"name": "Montserrat", "weight": "bold", "style": "normal"}, + "subtitle": {"name": "Open Sans", "weight": "light", "style": "normal"}, + "body": {"name": "Source Sans Pro", "weight": "normal", "style": "normal"}, + "accent": {"name": "Montserrat", "weight": "semibold", "style": "normal"} + }, + "brutalist_bold": { + "title": {"name": "Impact", "weight": "bold", "style": "normal"}, + "subtitle": {"name": "Arial Black", "weight": "normal", "style": "normal"}, + "body": {"name": "Helvetica", "weight": "normal", "style": "normal"}, + "accent": {"name": "Impact", "weight": "normal", "style": "normal"} + } + }, + "typography": { + "title": { + "font_name": "Segoe UI", + "font_size_large": 36, + "font_size_medium": 28, + "font_size_small": 24, + "bold": true + }, + "subtitle": { + "font_name": "Segoe UI Light", + "font_size_large": 20, + "font_size_medium": 18, + "font_size_small": 16, + "bold": false + }, + "body": { + "font_name": "Segoe UI", + "font_size_large": 16, + "font_size_medium": 14, + "font_size_small": 12, + "bold": false + }, + "caption": { + "font_name": "Segoe UI", + "font_size_large": 12, + "font_size_medium": 10, + "font_size_small": 9, + "bold": false + } + }, + "text_effects": { + "shadow_soft": { + "type": "shadow", + "blur_radius": 3.0, + "distance": 2.0, + "direction": 315.0, + "color": [0, 0, 0], + "transparency": 0.4 + }, + "shadow_strong": { + "type": "shadow", + "blur_radius": 6.0, + "distance": 4.0, + "direction": 315.0, + "color": [0, 0, 0], + "transparency": 0.6 + }, + "glow_subtle": { + "type": "glow", + "size": 3.0, + "color_role": "accent1", + "transparency": 0.3 + }, + "glow_vibrant": { + "type": "glow", + "size": 8.0, + "color_role": "accent2", + "transparency": 0.5 + }, + "outline_thin": { + "type": "outline", + "width": 1.0, + "color_role": "primary" + }, + "outline_thick": { + "type": "outline", + "width": 2.5, + "color_role": "secondary" + } + }, + "image_effects": { + "professional_shadow": { + "shadow": { + "blur_radius": 8.0, + "distance": 5.0, + "direction": 315.0, + "color": [0, 0, 0], + "transparency": 0.3 + }, + "border": { + "width": 2.0, + "color": [255, 255, 255] + } + }, + "modern_glow": { + "glow": { + "size": 6.0, + "color_role": "accent1", + "transparency": 0.4 + }, + "soft_edges": { + "radius": 4.0 + } + }, + "elegant_frame": { + "border": { + "width": 3.0, + "color_role": "primary" + }, + "reflection": { + "size": 0.3, + "transparency": 0.4 + } + }, + "custom_mask_circle": { + "mask_type": "circle", + "border_radius": "50%", + "shadow": { + "blur_radius": 12.0, + "distance": 6.0, + "color": [0, 0, 0], + "transparency": 0.25 + } + }, + "custom_mask_hexagon": { + "mask_type": "polygon", + "polygon_points": 6, + "glow": { + "size": 8.0, + "color_role": "accent1", + "transparency": 0.3 + } + }, + "neon_outline": { + "border": { + "width": 4.0, + "color_role": "accent1" + }, + "glow": { + "size": 10.0, + "color_role": "accent1", + "transparency": 0.6 + }, + "double_exposure": true + }, + "organic_blend": { + "blend_mode": "multiply", + "organic_mask": true, + "soft_edges": { + "radius": 8.0 + }, + "color_overlay": { + "color_role": "accent2", + "opacity": 0.2 + } + } + }, + "dynamic_sizing": { + "text_length_thresholds": { + "short": 50, + "medium": 150, + "long": 300, + "very_long": 500 + }, + "font_size_adjustments": { + "short": {"multiplier": 1.2, "min_size": 14, "max_size": 36}, + "medium": {"multiplier": 1.0, "min_size": 12, "max_size": 28}, + "long": {"multiplier": 0.9, "min_size": 10, "max_size": 24}, + "very_long": {"multiplier": 0.8, "min_size": 9, "max_size": 18} + }, + "line_spacing_adjustments": { + "short": 1.0, + "medium": 1.2, + "long": 1.3, + "very_long": 1.4 + } + }, + "auto_sizing_rules": { + "text_measurement": { + "characters_per_line": { + "title": 40, + "subtitle": 50, + "body": 60, + "caption": 70 + }, + "base_font_sizes": { + "title": {"min": 18, "max": 44, "default": 28}, + "subtitle": {"min": 14, "max": 24, "default": 18}, + "body": {"min": 10, "max": 18, "default": 14}, + "caption": {"min": 8, "max": 14, "default": 11} + } + }, + "dynamic_adjustments": { + "content_overflow": { + "action": "reduce_font_size", + "min_reduction": 1, + "max_reduction": 4 + }, + "content_underflow": { + "action": "increase_font_size", + "max_increase": 2 + }, + "line_wrapping": { + "enabled": true, + "break_long_words": false, + "preserve_formatting": true + } + } + }, + "effect_presets": { + "professional": { + "text_effects": ["shadow_soft"], + "image_effects": ["professional_shadow"], + "shape_effects": ["shadow_soft", "glow_subtle"] + }, + "modern": { + "text_effects": ["glow_subtle", "shadow_soft"], + "image_effects": ["modern_glow"], + "shape_effects": ["glow_vibrant", "shadow_strong"] + }, + "elegant": { + "text_effects": ["outline_thin", "shadow_soft"], + "image_effects": ["elegant_frame"], + "shape_effects": ["shadow_soft"] + }, + "neon_cyberpunk": { + "text_effects": ["glow_vibrant", "outline_thick"], + "image_effects": ["neon_outline"], + "shape_effects": ["glow_vibrant", "shadow_strong"] + }, + "organic_nature": { + "text_effects": ["shadow_soft"], + "image_effects": ["organic_blend"], + "shape_effects": ["shadow_soft", "glow_subtle"] + }, + "minimalist_clean": { + "text_effects": ["outline_thin"], + "image_effects": ["custom_mask_circle"], + "shape_effects": ["shadow_soft"] + }, + "brutalist_bold": { + "text_effects": ["shadow_strong", "outline_thick"], + "image_effects": ["custom_mask_hexagon"], + "shape_effects": ["shadow_strong", "glow_vibrant"] + } + }, + "templates": { + "title_slide": { + "name": "Dynamic Title Slide", + "description": "Main title slide with gradient background and text effects", + "layout_type": "title", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 1.0, + "top": 2.0, + "width": 8.0, + "height": 2.0 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_strong", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "gradient_text": true, + "gradient_colors": ["primary", "accent1"] + }, + "placeholder_text": "Transform Your Business", + "dynamic_content": { + "short_version": "Success", + "medium_version": "Business Success", + "long_version": "Transform Your Business for Success" + } + }, + { + "type": "text", + "role": "subtitle", + "position": { + "left": 1.0, + "top": 4.2, + "width": 8.0, + "height": 1.0 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "italic": true + }, + "placeholder_text": "Strategic Innovation for 2024", + "dynamic_content": { + "enhancement": "animated_fade_in" + } + }, + { + "type": "text", + "role": "author", + "position": { + "left": 1.0, + "top": 5.8, + "width": 8.0, + "height": 0.8 + }, + "styling": { + "font_type": "body", + "font_size": "medium", + "alignment": "center", + "color_role": "accent1", + "text_effects": ["outline_thin"], + "auto_wrap": true, + "bold": true + }, + "placeholder_text": "Leadership Team" + }, + { + "type": "shape", + "role": "decorative_accent", + "shape_type": "rectangle", + "position": { + "left": 3.5, + "top": 6.8, + "width": 3.0, + "height": 0.1 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "accent2", + "direction": "horizontal" + }, + "shadow": "shadow_soft", + "no_border": true + } + } + ], + "background": { + "type": "advanced_gradient", + "style": "radial", + "start_color_role": "light", + "end_color_role": "gradient_end", + "opacity": 0.8, + "overlay_pattern": "subtle_dots" + } + }, + "text_with_image": { + "name": "Dynamic Text + Image", + "description": "Text content with stylized image and interactive elements", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.4, + "width": 9.0, + "height": 1.0 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "left", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "underline": true, + "underline_color_role": "accent1" + }, + "placeholder_text": "Revolutionary Solutions" + }, + { + "type": "shape", + "role": "title_underline", + "shape_type": "rectangle", + "position": { + "left": 0.5, + "top": 1.3, + "width": 3.0, + "height": 0.08 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "primary", + "direction": "horizontal" + }, + "no_border": true, + "glow": "glow_subtle" + } + }, + { + "type": "text", + "role": "content", + "position": { + "left": 0.5, + "top": 1.8, + "width": 4.3, + "height": 4.8 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic", + "bullet_style": "custom", + "bullet_color_role": "accent1", + "bullet_shape": "diamond" + }, + "placeholder_text": "◆ Breakthrough innovation in technology\n◆ 250% increase in efficiency metrics\n◆ Sustainable solutions for the future\n◆ Industry-leading performance standards\n◆ Customer satisfaction at 98%", + "dynamic_formatting": { + "bullet_animation": "slide_in_left", + "text_highlight": "accent2", + "emphasis_words": ["breakthrough", "250%", "sustainable", "98%"] + } + }, + { + "type": "image", + "role": "supporting", + "position": { + "left": 5.2, + "top": 1.6, + "width": 4.3, + "height": 4.0 + }, + "styling": { + "effects": "professional_shadow", + "border_radius": 12, + "hover_effect": "scale_105", + "overlay_gradient": { + "color_role": "primary", + "opacity": 0.1, + "direction": "diagonal" + } + }, + "placeholder_text": "High-Impact Visual" + }, + { + "type": "shape", + "role": "image_frame", + "shape_type": "rectangle", + "position": { + "left": 5.0, + "top": 1.4, + "width": 4.7, + "height": 4.4 + }, + "styling": { + "fill_color": "transparent", + "line_color_role": "accent1", + "line_width": 3.0, + "border_radius": 15, + "glow": "glow_vibrant" + } + }, + { + "type": "text", + "role": "call_to_action", + "position": { + "left": 2.0, + "top": 6.0, + "width": 6.0, + "height": 0.8 + }, + "styling": { + "font_type": "accent", + "font_size": "large", + "alignment": "center", + "color_role": "accent2", + "text_effects": ["shadow_strong", "glow_vibrant"], + "auto_wrap": true, + "bold": true, + "italic": true + }, + "placeholder_text": "Ready to Transform? Let's Begin!" + } + ], + "background": { + "type": "layered_gradient", + "base_color_role": "light", + "accent_gradient": { + "start_color_role": "accent1", + "end_color_role": "transparent", + "opacity": 0.05, + "direction": "diagonal" + } + } + }, + "two_column_text": { + "name": "Two Columns of Text", + "description": "Two equal columns of text content with dynamic sizing", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Slide Title" + }, + { + "type": "text", + "role": "content_left", + "position": { + "left": 0.5, + "top": 1.5, + "width": 4.25, + "height": 5.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Column 1:\n• Point A\n• Point B\n• Point C\n• Supporting details" + }, + { + "type": "text", + "role": "content_right", + "position": { + "left": 5.25, + "top": 1.5, + "width": 4.25, + "height": 5.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Column 2:\n• Point D\n• Point E\n• Point F\n• Additional details" + } + ] + }, + "two_column_text_images": { + "name": "Two Columns Text + Images", + "description": "Two columns with text and corresponding images with effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Comparison Title" + }, + { + "type": "text", + "role": "content_left", + "position": { + "left": 0.5, + "top": 1.5, + "width": 4.25, + "height": 2.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Left Section:\n• Key point 1\n• Key point 2" + }, + { + "type": "image", + "role": "supporting_left", + "position": { + "left": 0.5, + "top": 4.2, + "width": 4.25, + "height": 2.3 + }, + "styling": { + "effects": "professional_shadow", + "border_radius": 8 + }, + "placeholder_text": "Left Image" + }, + { + "type": "text", + "role": "content_right", + "position": { + "left": 5.25, + "top": 1.5, + "width": 4.25, + "height": 2.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Right Section:\n• Key point 3\n• Key point 4" + }, + { + "type": "image", + "role": "supporting_right", + "position": { + "left": 5.25, + "top": 4.2, + "width": 4.25, + "height": 2.3 + }, + "styling": { + "effects": "professional_shadow", + "border_radius": 8 + }, + "placeholder_text": "Right Image" + } + ] + }, + "three_column_layout": { + "name": "Three Columns Text + Images", + "description": "Three equal columns with text and images with effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Three-Part Analysis" + }, + { + "type": "text", + "role": "content_1", + "position": { + "left": 0.5, + "top": 1.5, + "width": 2.8, + "height": 2.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Section 1:\n• Point A\n• Point B" + }, + { + "type": "image", + "role": "supporting_1", + "position": { + "left": 0.5, + "top": 3.7, + "width": 2.8, + "height": 2.8 + }, + "styling": { + "effects": "modern_glow", + "border_radius": 10 + }, + "placeholder_text": "Image 1" + }, + { + "type": "text", + "role": "content_2", + "position": { + "left": 3.6, + "top": 1.5, + "width": 2.8, + "height": 2.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Section 2:\n• Point C\n• Point D" + }, + { + "type": "image", + "role": "supporting_2", + "position": { + "left": 3.6, + "top": 3.7, + "width": 2.8, + "height": 2.8 + }, + "styling": { + "effects": "modern_glow", + "border_radius": 10 + }, + "placeholder_text": "Image 2" + }, + { + "type": "text", + "role": "content_3", + "position": { + "left": 6.7, + "top": 1.5, + "width": 2.8, + "height": 2.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Section 3:\n• Point E\n• Point F" + }, + { + "type": "image", + "role": "supporting_3", + "position": { + "left": 6.7, + "top": 3.7, + "width": 2.8, + "height": 2.8 + }, + "styling": { + "effects": "modern_glow", + "border_radius": 10 + }, + "placeholder_text": "Image 3" + } + ] + }, + "agenda_slide": { + "name": "Agenda/Directory Page", + "description": "Table of contents or agenda overview with styling", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Agenda" + }, + { + "type": "shape", + "role": "decorative", + "shape_type": "rectangle", + "position": { + "left": 1.0, + "top": 1.8, + "width": 8.0, + "height": 0.1 + }, + "styling": { + "fill_color_role": "accent1", + "no_border": true, + "glow": "glow_subtle" + } + }, + { + "type": "text", + "role": "agenda_items", + "position": { + "left": 2.0, + "top": 2.5, + "width": 6.0, + "height": 4.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "1. Introduction\n\n2. Problem Statement\n\n3. Proposed Solution\n\n4. Implementation Plan\n\n5. Timeline & Budget\n\n6. Q&A Session" + } + ], + "background": { + "type": "solid", + "color_role": "light" + } + }, + "chapter_intro": { + "name": "Chapter Introduction Page", + "description": "Section divider with chapter number and title with effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "shape", + "role": "background_accent", + "shape_type": "rectangle", + "position": { + "left": 0.0, + "top": 0.0, + "width": 4.0, + "height": 7.5 + }, + "styling": { + "fill_gradient": { + "start_color_role": "primary", + "end_color_role": "accent1", + "direction": "diagonal" + }, + "no_border": true + } + }, + { + "type": "text", + "role": "chapter_number", + "position": { + "left": 0.5, + "top": 2.0, + "width": 3.0, + "height": 1.5 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "03" + }, + { + "type": "text", + "role": "chapter_title", + "position": { + "left": 4.5, + "top": 2.5, + "width": 5.0, + "height": 2.5 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "left", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Chapter Title" + }, + { + "type": "text", + "role": "chapter_description", + "position": { + "left": 4.5, + "top": 5.0, + "width": 5.0, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Brief description of what this chapter covers" + } + ] + }, + "thank_you_slide": { + "name": "Thank You/End Page", + "description": "Closing slide with contact information and effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "thank_you", + "position": { + "left": 1.0, + "top": 2.0, + "width": 8.0, + "height": 1.5 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_strong", "glow_vibrant"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Thank You" + }, + { + "type": "text", + "role": "questions", + "position": { + "left": 1.0, + "top": 3.8, + "width": 8.0, + "height": 1.0 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "italic": true + }, + "placeholder_text": "Questions & Discussion" + }, + { + "type": "text", + "role": "contact", + "position": { + "left": 2.0, + "top": 5.5, + "width": 6.0, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Contact Information:\nemail@company.com\nphone: (555) 123-4567" + } + ], + "background": { + "type": "professional_gradient", + "style": "subtle", + "direction": "horizontal" + } + }, + "timeline_slide": { + "name": "Timeline Page", + "description": "Horizontal timeline with milestones and effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Project Timeline" + }, + { + "type": "shape", + "role": "timeline_line", + "shape_type": "rectangle", + "position": { + "left": 1.0, + "top": 3.5, + "width": 8.0, + "height": 0.1 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "accent2", + "direction": "horizontal" + }, + "no_border": true, + "glow": "glow_subtle" + } + }, + { + "type": "shape", + "role": "milestone_1", + "shape_type": "oval", + "position": { + "left": 1.5, + "top": 3.25, + "width": 0.5, + "height": 0.5 + }, + "styling": { + "fill_color_role": "primary", + "line_color_role": "primary", + "shadow": "shadow_soft" + } + }, + { + "type": "text", + "role": "milestone_1_text", + "position": { + "left": 1.0, + "top": 4.0, + "width": 1.5, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Phase 1\nQ1 2024" + }, + { + "type": "shape", + "role": "milestone_2", + "shape_type": "oval", + "position": { + "left": 3.5, + "top": 3.25, + "width": 0.5, + "height": 0.5 + }, + "styling": { + "fill_color_role": "primary", + "line_color_role": "primary", + "shadow": "shadow_soft" + } + }, + { + "type": "text", + "role": "milestone_2_text", + "position": { + "left": 3.0, + "top": 4.0, + "width": 1.5, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Phase 2\nQ2 2024" + }, + { + "type": "shape", + "role": "milestone_3", + "shape_type": "oval", + "position": { + "left": 5.5, + "top": 3.25, + "width": 0.5, + "height": 0.5 + }, + "styling": { + "fill_color_role": "primary", + "line_color_role": "primary", + "shadow": "shadow_soft" + } + }, + { + "type": "text", + "role": "milestone_3_text", + "position": { + "left": 5.0, + "top": 4.0, + "width": 1.5, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Phase 3\nQ3 2024" + }, + { + "type": "shape", + "role": "milestone_4", + "shape_type": "oval", + "position": { + "left": 7.5, + "top": 3.25, + "width": 0.5, + "height": 0.5 + }, + "styling": { + "fill_color_role": "accent2", + "line_color_role": "accent2", + "shadow": "shadow_soft", + "glow": "glow_vibrant" + } + }, + { + "type": "text", + "role": "milestone_4_text", + "position": { + "left": 7.0, + "top": 4.0, + "width": 1.5, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Launch\nQ4 2024" + } + ] + }, + "data_table_slide": { + "name": "Data Table Page", + "description": "Slide focused on tabular data presentation with styling", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Data Analysis Results" + }, + { + "type": "table", + "role": "main_data", + "position": { + "left": 1.0, + "top": 1.8, + "width": 8.0, + "height": 4.2 + }, + "table_config": { + "rows": 5, + "cols": 4, + "header_row": true, + "data": [ + ["Metric", "Q1", "Q2", "Q3"], + ["Revenue", "$1.2M", "$1.5M", "$1.8M"], + ["Growth", "15%", "25%", "20%"], + ["Customers", "1,200", "1,500", "1,800"], + ["Satisfaction", "4.2/5", "4.4/5", "4.6/5"] + ] + }, + "styling": { + "header_bg_color_role": "primary", + "header_text_color": [255, 255, 255], + "body_bg_color_role": "light", + "border_color_role": "secondary", + "shadow": "shadow_soft" + } + }, + { + "type": "text", + "role": "insights", + "position": { + "left": 1.0, + "top": 6.2, + "width": 8.0, + "height": 1.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "italic": true + }, + "placeholder_text": "Key insight: Consistent growth across all metrics with strong customer satisfaction" + } + ] + }, + "chart_comparison": { + "name": "Chart Comparison Page", + "description": "Two charts side by side for comparison with effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Performance Comparison" + }, + { + "type": "chart", + "role": "chart_left", + "position": { + "left": 0.5, + "top": 1.5, + "width": 4.25, + "height": 4.5 + }, + "chart_config": { + "type": "column", + "title": "Before Implementation", + "categories": ["Jan", "Feb", "Mar", "Apr"], + "series": [ + { + "name": "Sales", + "values": [100, 120, 110, 130] + } + ] + }, + "styling": { + "color_scheme": "corporate_gray", + "shadow": "shadow_soft" + } + }, + { + "type": "chart", + "role": "chart_right", + "position": { + "left": 5.25, + "top": 1.5, + "width": 4.25, + "height": 4.5 + }, + "chart_config": { + "type": "column", + "title": "After Implementation", + "categories": ["Jan", "Feb", "Mar", "Apr"], + "series": [ + { + "name": "Sales", + "values": [140, 170, 160, 190] + } + ] + }, + "styling": { + "color_scheme": "modern_blue", + "shadow": "shadow_soft" + } + }, + { + "type": "text", + "role": "comparison_note", + "position": { + "left": 2.0, + "top": 6.2, + "width": 6.0, + "height": 0.8 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "accent1", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "35% average improvement across all metrics" + } + ] + }, + "full_image_slide": { + "name": "Full Image with Overlay Text", + "description": "Large background image with text overlay and effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "image", + "role": "background", + "position": { + "left": 0.0, + "top": 0.0, + "width": 10.0, + "height": 7.5 + }, + "styling": { + "transparency": 0.3, + "overlay_gradient": { + "color_role": "primary", + "opacity": 0.2, + "direction": "vertical" + } + }, + "placeholder_text": "Full Background Image" + }, + { + "type": "shape", + "role": "text_overlay_bg", + "shape_type": "rectangle", + "position": { + "left": 1.0, + "top": 2.0, + "width": 8.0, + "height": 3.5 + }, + "styling": { + "fill_color": [0, 0, 0], + "transparency": 0.6, + "no_border": true, + "border_radius": 15 + } + }, + { + "type": "text", + "role": "overlay_title", + "position": { + "left": 1.5, + "top": 2.5, + "width": 7.0, + "height": 1.2 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong", "glow_vibrant"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "Impactful Statement" + }, + { + "type": "text", + "role": "overlay_subtitle", + "position": { + "left": 1.5, + "top": 3.8, + "width": 7.0, + "height": 1.2 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Supporting message or call to action" + } + ] + }, + "process_flow": { + "name": "Process Flow Diagram", + "description": "Step-by-step process visualization with enhanced effects", + "layout_type": "content", + "typography_style": "tech_modern", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.3, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Streamlined Process Architecture" + }, + { + "type": "shape", + "role": "step_1_container", + "shape_type": "rounded_rectangle", + "position": { + "left": 0.5, + "top": 1.8, + "width": 2.0, + "height": 1.5 + }, + "styling": { + "fill_gradient": { + "start_color_role": "primary", + "end_color_role": "accent1", + "direction": "diagonal" + }, + "shadow": "shadow_strong", + "glow": "glow_subtle", + "border_radius": 15, + "no_border": true + } + }, + { + "type": "text", + "role": "step_1_number", + "position": { + "left": 0.6, + "top": 1.0, + "width": 0.8, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "background_shape": { + "type": "oval", + "color_role": "accent2", + "shadow": "shadow_strong" + } + }, + "placeholder_text": "01" + }, + { + "type": "text", + "role": "step_1_text", + "position": { + "left": 0.5, + "top": 1.8, + "width": 2.0, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "vertical_alignment": "middle" + }, + "placeholder_text": "Data\nCollection\n& Analysis" + }, + { + "type": "shape", + "role": "connector_1", + "shape_type": "arrow", + "position": { + "left": 2.7, + "top": 2.4, + "width": 1.0, + "height": 0.4 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "accent2", + "direction": "horizontal" + }, + "glow": "glow_vibrant", + "shadow": "shadow_soft", + "no_border": true + } + }, + { + "type": "shape", + "role": "step_2_container", + "shape_type": "rounded_rectangle", + "position": { + "left": 4.0, + "top": 1.8, + "width": 2.0, + "height": 1.5 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "secondary", + "direction": "diagonal" + }, + "shadow": "shadow_strong", + "glow": "glow_subtle", + "border_radius": 15, + "no_border": true + } + }, + { + "type": "text", + "role": "step_2_number", + "position": { + "left": 4.1, + "top": 1.0, + "width": 0.8, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "background_shape": { + "type": "oval", + "color_role": "accent2", + "shadow": "shadow_strong" + } + }, + "placeholder_text": "02" + }, + { + "type": "text", + "role": "step_2_text", + "position": { + "left": 4.0, + "top": 1.8, + "width": 2.0, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "vertical_alignment": "middle" + }, + "placeholder_text": "AI-Powered\nProcessing\n& Optimization" + }, + { + "type": "shape", + "role": "connector_2", + "shape_type": "arrow", + "position": { + "left": 6.2, + "top": 2.4, + "width": 1.0, + "height": 0.4 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "accent2", + "direction": "horizontal" + }, + "glow": "glow_vibrant", + "shadow": "shadow_soft", + "no_border": true + } + }, + { + "type": "shape", + "role": "step_3_container", + "shape_type": "rounded_rectangle", + "position": { + "left": 7.5, + "top": 1.8, + "width": 2.0, + "height": 1.5 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent2", + "end_color_role": "primary", + "direction": "diagonal" + }, + "shadow": "shadow_strong", + "glow": "glow_vibrant", + "border_radius": 15, + "no_border": true + } + }, + { + "type": "text", + "role": "step_3_number", + "position": { + "left": 7.6, + "top": 1.0, + "width": 0.8, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "background_shape": { + "type": "oval", + "color_role": "primary", + "shadow": "shadow_strong" + } + }, + "placeholder_text": "03" + }, + { + "type": "text", + "role": "step_3_text", + "position": { + "left": 7.5, + "top": 1.8, + "width": 2.0, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "vertical_alignment": "middle" + }, + "placeholder_text": "Intelligent\nDelivery\n& Results" + }, + { + "type": "text", + "role": "process_benefits", + "position": { + "left": 1.0, + "top": 4.0, + "width": 8.0, + "height": 2.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic", + "background_shape": { + "type": "rounded_rectangle", + "color_role": "light", + "opacity": 0.8, + "shadow": "shadow_soft" + } + }, + "placeholder_text": "🎯 Complete automation reduces processing time by 85%\n⚡ Real-time optimization delivers instant insights\n🔄 Continuous learning improves accuracy over time\n📊 Comprehensive analytics provide actionable intelligence\n🚀 Scalable architecture grows with your business needs", + "dynamic_formatting": { + "icon_glow": "glow_subtle", + "emphasis_words": ["85%", "real-time", "continuous", "comprehensive", "scalable"] + } + } + ], + "background": { + "type": "tech_gradient", + "base_gradient": { + "start_color_role": "light", + "end_color_role": "secondary", + "direction": "diagonal", + "opacity": 0.1 + }, + "tech_pattern": "circuit_subtle" + } + }, + "quote_testimonial": { + "name": "Quote/Testimonial Slide", + "description": "Featured quote or customer testimonial with effects", + "layout_type": "content", + "typography_style": "elegant_serif", + "elements": [ + { + "type": "shape", + "role": "quote_mark", + "shape_type": "cloud", + "position": { + "left": 1.0, + "top": 1.5, + "width": 1.0, + "height": 1.0 + }, + "styling": { + "fill_color_role": "accent1", + "no_border": true, + "transparency": 0.3, + "glow": "glow_subtle" + } + }, + { + "type": "text", + "role": "quote_text", + "position": { + "left": 1.5, + "top": 2.5, + "width": 7.0, + "height": 2.5 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "italic": true + }, + "placeholder_text": "\"This solution transformed our business operations and increased efficiency by 40%. We couldn't be more satisfied with the results.\"" + }, + { + "type": "text", + "role": "attribution", + "position": { + "left": 2.0, + "top": 5.5, + "width": 6.0, + "height": 1.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "— Jane Smith, CEO, Company Name" + } + ], + "background": { + "type": "professional_gradient", + "style": "subtle", + "direction": "vertical" + } + }, + "key_metrics_dashboard": { + "name": "KPI Dashboard", + "description": "Interactive metrics dashboard with animated counters and effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.2, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "gradient_text": true, + "gradient_colors": ["primary", "accent1"] + }, + "placeholder_text": "Performance Dashboard 2024" + }, + { + "type": "shape", + "role": "metric_1_container", + "shape_type": "rounded_rectangle", + "position": { + "left": 0.5, + "top": 1.3, + "width": 2.2, + "height": 2.2 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "primary", + "direction": "diagonal", + "angle": 45 + }, + "shadow": "shadow_strong", + "glow": "glow_subtle", + "border_radius": 20, + "no_border": true + } + }, + { + "type": "text", + "role": "metric_1_value", + "position": { + "left": 0.5, + "top": 1.6, + "width": 2.2, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "94%", + "dynamic_content": { + "animation": "count_up", + "duration": 2000, + "from_value": 0 + } + }, + { + "type": "text", + "role": "metric_1_label", + "position": { + "left": 0.5, + "top": 2.5, + "width": 2.2, + "height": 0.6 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Customer\nSatisfaction" + }, + { + "type": "shape", + "role": "metric_2_container", + "shape_type": "rounded_rectangle", + "position": { + "left": 3.2, + "top": 1.3, + "width": 2.2, + "height": 2.2 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent2", + "end_color_role": "primary", + "direction": "diagonal", + "angle": 135 + }, + "shadow": "shadow_strong", + "glow": "glow_subtle", + "border_radius": 20, + "no_border": true + } + }, + { + "type": "text", + "role": "metric_2_value", + "position": { + "left": 3.2, + "top": 1.6, + "width": 2.2, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "$2.4M", + "dynamic_content": { + "animation": "count_up", + "duration": 2500, + "format": "currency" + } + }, + { + "type": "text", + "role": "metric_2_label", + "position": { + "left": 3.2, + "top": 2.5, + "width": 2.2, + "height": 0.6 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Annual\nRevenue" + }, + { + "type": "shape", + "role": "metric_3_container", + "shape_type": "rounded_rectangle", + "position": { + "left": 5.9, + "top": 1.3, + "width": 2.2, + "height": 2.2 + }, + "styling": { + "fill_gradient": { + "start_color_role": "secondary", + "end_color_role": "accent1", + "direction": "radial" + }, + "shadow": "shadow_strong", + "glow": "glow_subtle", + "border_radius": 20, + "no_border": true + } + }, + { + "type": "text", + "role": "metric_3_value", + "position": { + "left": 5.9, + "top": 1.6, + "width": 2.2, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "247", + "dynamic_content": { + "animation": "count_up", + "duration": 1800 + } + }, + { + "type": "text", + "role": "metric_3_label", + "position": { + "left": 5.9, + "top": 2.5, + "width": 2.2, + "height": 0.6 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "New\nCustomers" + }, + { + "type": "chart", + "role": "trend_visualization", + "position": { + "left": 1.0, + "top": 4.0, + "width": 8.0, + "height": 3.0 + }, + "chart_config": { + "type": "line_smooth", + "title": "Performance Trend Analysis", + "categories": ["Q1", "Q2", "Q3", "Q4"], + "series": [ + { + "name": "Revenue Growth", + "values": [100, 125, 150, 180], + "color_role": "accent1", + "line_style": "smooth_gradient" + }, + { + "name": "Customer Acquisition", + "values": [80, 95, 120, 145], + "color_role": "accent2", + "line_style": "smooth_gradient" + } + ] + }, + "styling": { + "background_gradient": { + "start_color_role": "light", + "end_color_role": "accent1", + "opacity": 0.1 + }, + "shadow": "shadow_soft" + } + } + ], + "background": { + "type": "premium_gradient", + "base_gradient": { + "start_color_role": "light", + "end_color_role": "gradient_end", + "direction": "radial", + "opacity": 0.3 + }, + "overlay_pattern": "geometric_subtle" + } + }, + "before_after_comparison": { + "name": "Before/After Comparison", + "description": "Dynamic comparison layout with visual dividers and effects", + "layout_type": "content", + "typography_style": "tech_modern", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.3, + "width": 9.0, + "height": 0.9 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "outline_thin"], + "auto_wrap": true, + "auto_fit": true, + "letter_spacing": 1.2 + }, + "placeholder_text": "Before vs After Transformation" + }, + { + "type": "shape", + "role": "vs_divider", + "shape_type": "oval", + "position": { + "left": 4.6, + "top": 3.0, + "width": 0.8, + "height": 0.8 + }, + "styling": { + "fill_color_role": "accent2", + "line_color_role": "primary", + "line_width": 3.0, + "glow": "glow_vibrant", + "shadow": "shadow_strong" + } + }, + { + "type": "text", + "role": "vs_text", + "position": { + "left": 4.6, + "top": 3.0, + "width": 0.8, + "height": 0.8 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "vertical_alignment": "middle" + }, + "placeholder_text": "VS" + }, + { + "type": "text", + "role": "left_header", + "position": { + "left": 0.5, + "top": 1.4, + "width": 4.0, + "height": 0.6 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color_role": "secondary", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "background_shape": { + "type": "rounded_rectangle", + "color_role": "light", + "opacity": 0.8 + } + }, + "placeholder_text": "BEFORE" + }, + { + "type": "text", + "role": "content_left", + "position": { + "left": 0.5, + "top": 2.1, + "width": 4.0, + "height": 3.8 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic", + "bullet_style": "custom", + "bullet_color_role": "secondary", + "negative_emphasis": true + }, + "placeholder_text": "✗ Manual processes taking 8+ hours\n✗ 40% error rate in operations\n✗ Limited scalability options\n✗ Customer complaints increasing\n✗ High operational costs\n✗ Inefficient resource allocation", + "dynamic_formatting": { + "negative_indicators": ["✗", "manual", "error", "limited", "complaints", "high", "inefficient"], + "text_color_negative": "secondary" + } + }, + { + "type": "text", + "role": "right_header", + "position": { + "left": 5.5, + "top": 1.4, + "width": 4.0, + "height": 0.6 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "background_shape": { + "type": "rounded_rectangle", + "color_role": "accent1", + "opacity": 0.2 + } + }, + "placeholder_text": "AFTER" + }, + { + "type": "text", + "role": "content_right", + "position": { + "left": 5.5, + "top": 2.1, + "width": 4.0, + "height": 3.8 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic", + "bullet_style": "custom", + "bullet_color_role": "primary", + "positive_emphasis": true + }, + "placeholder_text": "✓ Automated workflows in 2 hours\n✓ 2% error rate with AI validation\n✓ Infinite scalability in cloud\n✓ 98% customer satisfaction score\n✓ 60% reduction in operational costs\n✓ Optimized resource management", + "dynamic_formatting": { + "positive_indicators": ["✓", "automated", "AI", "infinite", "98%", "60% reduction", "optimized"], + "text_color_positive": "primary", + "emphasis_glow": "glow_subtle" + } + }, + { + "type": "shape", + "role": "improvement_arrow", + "shape_type": "arrow", + "position": { + "left": 3.8, + "top": 6.2, + "width": 2.4, + "height": 0.6 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "primary", + "direction": "horizontal" + }, + "glow": "glow_vibrant", + "shadow": "shadow_strong", + "no_border": true + } + }, + { + "type": "text", + "role": "improvement_text", + "position": { + "left": 3.0, + "top": 6.9, + "width": 4.0, + "height": 0.5 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "center", + "color_role": "accent2", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "italic": true + }, + "placeholder_text": "75% Overall Improvement" + } + ], + "background": { + "type": "split_gradient", + "left_gradient": { + "start_color_role": "secondary", + "end_color_role": "light", + "opacity": 0.1 + }, + "right_gradient": { + "start_color_role": "primary", + "end_color_role": "light", + "opacity": 0.1 + } + } + }, + "team_introduction": { + "name": "Team Introduction", + "description": "Team member showcase with photos, roles and effects", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "title", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 0.8 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Meet the Team" + }, + { + "type": "image", + "role": "team_member_1", + "position": { + "left": 1.0, + "top": 2.0, + "width": 2.0, + "height": 2.0 + }, + "styling": { + "effects": "elegant_frame", + "border_radius": "50%", + "hover_effect": "scale_105" + }, + "placeholder_text": "Team Member 1" + }, + { + "type": "text", + "role": "member_1_name", + "position": { + "left": 1.0, + "top": 4.2, + "width": 2.0, + "height": 0.4 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "Alice Johnson" + }, + { + "type": "text", + "role": "member_1_role", + "position": { + "left": 1.0, + "top": 4.7, + "width": 2.0, + "height": 0.4 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Project Manager" + }, + { + "type": "image", + "role": "team_member_2", + "position": { + "left": 4.0, + "top": 2.0, + "width": 2.0, + "height": 2.0 + }, + "styling": { + "effects": "elegant_frame", + "border_radius": "50%", + "hover_effect": "scale_105" + }, + "placeholder_text": "Team Member 2" + }, + { + "type": "text", + "role": "member_2_name", + "position": { + "left": 4.0, + "top": 4.2, + "width": 2.0, + "height": 0.4 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "Bob Smith" + }, + { + "type": "text", + "role": "member_2_role", + "position": { + "left": 4.0, + "top": 4.7, + "width": 2.0, + "height": 0.4 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Lead Developer" + }, + { + "type": "image", + "role": "team_member_3", + "position": { + "left": 7.0, + "top": 2.0, + "width": 2.0, + "height": 2.0 + }, + "styling": { + "effects": "elegant_frame", + "border_radius": "50%", + "hover_effect": "scale_105" + }, + "placeholder_text": "Team Member 3" + }, + { + "type": "text", + "role": "member_3_name", + "position": { + "left": 7.0, + "top": 4.2, + "width": 2.0, + "height": 0.4 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "Carol Davis" + }, + { + "type": "text", + "role": "member_3_role", + "position": { + "left": 7.0, + "top": 4.7, + "width": 2.0, + "height": 0.4 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "UX Designer" + }, + { + "type": "text", + "role": "team_description", + "position": { + "left": 1.5, + "top": 5.5, + "width": 7.0, + "height": 1.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "Our experienced team combines technical expertise with creative vision to deliver exceptional results for every project." + } + ] + }, + "minimalist_hero": { + "name": "Minimalist Hero Slide", + "description": "Clean, spacious design with bold typography and minimal elements", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "hero_title", + "position": { + "left": 1.0, + "top": 2.5, + "width": 8.0, + "height": 2.5 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["outline_thin"], + "auto_wrap": true, + "auto_fit": true, + "letter_spacing": 1.5, + "line_height": 1.2 + }, + "placeholder_text": "Less is More", + "dynamic_content": { + "responsive_sizing": true, + "min_font_size": 32, + "max_font_size": 72 + } + }, + { + "type": "shape", + "role": "accent_line", + "shape_type": "rectangle", + "position": { + "left": 4.0, + "top": 5.2, + "width": 2.0, + "height": 0.05 + }, + "styling": { + "fill_color_role": "accent1", + "no_border": true + } + } + ], + "background": { + "type": "solid", + "color_role": "light", + "texture": "paper_subtle" + } + }, + "neon_cyberpunk": { + "name": "Neon Cyberpunk Slide", + "description": "Futuristic design with neon colors and cyber elements", + "layout_type": "content", + "typography_style": "tech_modern", + "elements": [ + { + "type": "text", + "role": "cyber_title", + "position": { + "left": 0.5, + "top": 1.0, + "width": 9.0, + "height": 1.5 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "accent1", + "text_effects": ["glow_vibrant", "outline_thick"], + "auto_wrap": true, + "auto_fit": true, + "text_transform": "uppercase", + "letter_spacing": 2.0 + }, + "placeholder_text": "FUTURE TECH", + "dynamic_content": { + "neon_flicker": true, + "animation": "glow_pulse" + } + }, + { + "type": "shape", + "role": "cyber_grid", + "shape_type": "rectangle", + "position": { + "left": 0.0, + "top": 0.0, + "width": 10.0, + "height": 7.5 + }, + "styling": { + "fill_color": "transparent", + "line_color_role": "accent1", + "line_width": 1.0, + "opacity": 0.3, + "pattern": "grid_cyber" + } + }, + { + "type": "text", + "role": "cyber_content", + "position": { + "left": 1.0, + "top": 3.0, + "width": 8.0, + "height": 3.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "light", + "text_effects": ["glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "◇ Neural network integration\n◇ Quantum processing capabilities\n◇ Holographic interface design\n◇ AI-driven optimization", + "dynamic_formatting": { + "bullet_glow": true, + "typewriter_effect": true + } + } + ], + "background": { + "type": "cyber_gradient", + "base_color": [10, 10, 15], + "accent_gradient": { + "start_color_role": "accent1", + "end_color_role": "accent2", + "opacity": 0.2, + "direction": "diagonal" + }, + "tech_pattern": "circuit_matrix" + } + }, + "nature_organic": { + "name": "Organic Nature Slide", + "description": "Earth-toned design with organic shapes and natural textures", + "layout_type": "content", + "typography_style": "organic_flow", + "elements": [ + { + "type": "text", + "role": "nature_title", + "position": { + "left": 1.0, + "top": 1.0, + "width": 8.0, + "height": 1.2 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "left", + "color_role": "primary", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "organic_curve": true + }, + "placeholder_text": "Sustainable Growth" + }, + { + "type": "shape", + "role": "organic_blob_1", + "shape_type": "organic_blob", + "position": { + "left": 7.0, + "top": 0.5, + "width": 2.5, + "height": 2.0 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "accent2", + "direction": "radial" + }, + "opacity": 0.7, + "no_border": true + } + }, + { + "type": "text", + "role": "nature_content", + "position": { + "left": 1.0, + "top": 2.5, + "width": 5.5, + "height": 4.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "🌱 Renewable energy solutions\n🌍 Carbon-neutral operations\n🌿 Biodegradable materials\n🌊 Water conservation systems" + }, + { + "type": "shape", + "role": "organic_blob_2", + "shape_type": "organic_blob", + "position": { + "left": 0.2, + "top": 5.0, + "width": 3.0, + "height": 2.0 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent2", + "end_color_role": "primary", + "direction": "radial" + }, + "opacity": 0.5, + "no_border": true + } + } + ], + "background": { + "type": "organic_gradient", + "base_color_role": "light", + "texture": "organic_paper", + "overlay_pattern": "leaves_subtle" + } + }, + "interactive_poll": { + "name": "Interactive Poll Slide", + "description": "Engaging poll slide with interactive elements and real-time visualization", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "poll_question", + "position": { + "left": 0.5, + "top": 0.5, + "width": 9.0, + "height": 1.2 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "What's your biggest challenge?", + "dynamic_content": { + "interactive": true, + "poll_integration": true + } + }, + { + "type": "shape", + "role": "poll_option_1", + "shape_type": "rounded_rectangle", + "position": { + "left": 1.0, + "top": 2.0, + "width": 8.0, + "height": 0.8 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "primary", + "direction": "horizontal" + }, + "border_radius": 25, + "shadow": "shadow_soft", + "interactive": true, + "hover_effect": "scale_102" + } + }, + { + "type": "text", + "role": "poll_option_1_text", + "position": { + "left": 1.2, + "top": 2.1, + "width": 6.0, + "height": 0.6 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "A) Time Management" + }, + { + "type": "text", + "role": "poll_option_1_percentage", + "position": { + "left": 7.5, + "top": 2.1, + "width": 1.3, + "height": 0.6 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "right", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "45%", + "dynamic_content": { + "animation": "count_up", + "live_update": true + } + }, + { + "type": "shape", + "role": "poll_option_2", + "shape_type": "rounded_rectangle", + "position": { + "left": 1.0, + "top": 3.0, + "width": 6.0, + "height": 0.8 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent2", + "end_color_role": "secondary", + "direction": "horizontal" + }, + "border_radius": 25, + "shadow": "shadow_soft", + "interactive": true, + "hover_effect": "scale_102" + } + }, + { + "type": "text", + "role": "poll_option_2_text", + "position": { + "left": 1.2, + "top": 3.1, + "width": 4.5, + "height": 0.6 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "B) Budget Constraints" + }, + { + "type": "text", + "role": "poll_option_2_percentage", + "position": { + "left": 6.0, + "top": 3.1, + "width": 1.0, + "height": 0.6 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "right", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "30%", + "dynamic_content": { + "animation": "count_up", + "live_update": true + } + }, + { + "type": "shape", + "role": "poll_option_3", + "shape_type": "rounded_rectangle", + "position": { + "left": 1.0, + "top": 4.0, + "width": 5.0, + "height": 0.8 + }, + "styling": { + "fill_gradient": { + "start_color_role": "primary", + "end_color_role": "accent1", + "direction": "horizontal" + }, + "border_radius": 25, + "shadow": "shadow_soft", + "interactive": true, + "hover_effect": "scale_102" + } + }, + { + "type": "text", + "role": "poll_option_3_text", + "position": { + "left": 1.2, + "top": 4.1, + "width": 3.5, + "height": 0.6 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "C) Team Communication" + }, + { + "type": "text", + "role": "poll_option_3_percentage", + "position": { + "left": 5.0, + "top": 4.1, + "width": 1.0, + "height": 0.6 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "right", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "25%", + "dynamic_content": { + "animation": "count_up", + "live_update": true + } + }, + { + "type": "text", + "role": "poll_instruction", + "position": { + "left": 1.0, + "top": 5.5, + "width": 8.0, + "height": 1.0 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "center", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "italic": true + }, + "placeholder_text": "🗳️ Vote now using your mobile device or click on an option" + } + ], + "background": { + "type": "interactive_gradient", + "base_color_role": "light", + "pulse_effect": true + } + }, + "split_screen_comparison": { + "name": "Split Screen Comparison", + "description": "Modern split-screen layout for comparing two concepts or solutions", + "layout_type": "content", + "typography_style": "tech_modern", + "elements": [ + { + "type": "text", + "role": "main_title", + "position": { + "left": 0.5, + "top": 0.3, + "width": 9.0, + "height": 0.9 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "outline_thin"], + "auto_wrap": true, + "auto_fit": true + }, + "placeholder_text": "Choose Your Path" + }, + { + "type": "shape", + "role": "left_section_bg", + "shape_type": "rectangle", + "position": { + "left": 0.0, + "top": 1.5, + "width": 4.9, + "height": 6.0 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "primary", + "direction": "diagonal" + }, + "opacity": 0.9, + "no_border": true + } + }, + { + "type": "shape", + "role": "right_section_bg", + "shape_type": "rectangle", + "position": { + "left": 5.1, + "top": 1.5, + "width": 4.9, + "height": 6.0 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent2", + "end_color_role": "secondary", + "direction": "diagonal" + }, + "opacity": 0.9, + "no_border": true + } + }, + { + "type": "text", + "role": "left_title", + "position": { + "left": 0.2, + "top": 2.0, + "width": 4.5, + "height": 0.8 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "TRADITIONAL" + }, + { + "type": "text", + "role": "right_title", + "position": { + "left": 5.3, + "top": 2.0, + "width": 4.5, + "height": 0.8 + }, + "styling": { + "font_type": "subtitle", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "INNOVATIVE" + }, + { + "type": "text", + "role": "left_content", + "position": { + "left": 0.3, + "top": 3.0, + "width": 4.3, + "height": 3.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "• Manual processes\n• Limited scalability\n• Higher costs\n• Slower execution\n• Traditional methods" + }, + { + "type": "text", + "role": "right_content", + "position": { + "left": 5.4, + "top": 3.0, + "width": 4.3, + "height": 3.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "• Automated workflows\n• Infinite scalability\n• Cost optimization\n• Rapid deployment\n• AI-powered solutions" + }, + { + "type": "shape", + "role": "divider_line", + "shape_type": "rectangle", + "position": { + "left": 4.95, + "top": 1.5, + "width": 0.1, + "height": 6.0 + }, + "styling": { + "fill_color": [255, 255, 255], + "opacity": 0.8, + "no_border": true + } + } + ], + "background": { + "type": "split_gradient", + "left_gradient": { + "start_color_role": "accent1", + "end_color_role": "light", + "opacity": 0.1 + }, + "right_gradient": { + "start_color_role": "accent2", + "end_color_role": "light", + "opacity": 0.1 + } + } + }, + "product_showcase": { + "name": "Product Showcase", + "description": "Modern product presentation with custom image masks and interactive elements", + "layout_type": "content", + "typography_style": "modern_sans", + "elements": [ + { + "type": "text", + "role": "product_title", + "position": { + "left": 0.5, + "top": 0.3, + "width": 9.0, + "height": 1.0 + }, + "styling": { + "font_type": "title", + "font_size": "dynamic", + "alignment": "center", + "color_role": "primary", + "text_effects": ["shadow_soft", "glow_subtle"], + "auto_wrap": true, + "auto_fit": true, + "gradient_text": true, + "gradient_colors": ["primary", "accent1"] + }, + "placeholder_text": "Revolutionary Product" + }, + { + "type": "image", + "role": "hero_product", + "position": { + "left": 1.0, + "top": 1.8, + "width": 4.0, + "height": 4.0 + }, + "styling": { + "effects": "custom_mask_hexagon", + "hover_effect": "rotate_360", + "interactive": true, + "zoom_on_click": true + }, + "placeholder_text": "Product Hero Image" + }, + { + "type": "text", + "role": "product_features", + "position": { + "left": 5.5, + "top": 1.8, + "width": 4.0, + "height": 2.5 + }, + "styling": { + "font_type": "body", + "font_size": "dynamic", + "alignment": "left", + "color_role": "text", + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "line_spacing": "dynamic" + }, + "placeholder_text": "✨ Cutting-edge technology\n🚀 10x performance boost\n🔒 Enterprise-grade security\n🌐 Global compatibility\n📱 Mobile-first design" + }, + { + "type": "shape", + "role": "feature_highlight_1", + "shape_type": "rounded_rectangle", + "position": { + "left": 5.5, + "top": 4.5, + "width": 1.8, + "height": 1.2 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent1", + "end_color_role": "primary", + "direction": "diagonal" + }, + "border_radius": 15, + "shadow": "shadow_soft", + "glow": "glow_subtle" + } + }, + { + "type": "text", + "role": "highlight_1_text", + "position": { + "left": 5.6, + "top": 4.8, + "width": 1.6, + "height": 0.6 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "99.9%\nUptime" + }, + { + "type": "shape", + "role": "feature_highlight_2", + "shape_type": "rounded_rectangle", + "position": { + "left": 7.5, + "top": 4.5, + "width": 1.8, + "height": 1.2 + }, + "styling": { + "fill_gradient": { + "start_color_role": "accent2", + "end_color_role": "secondary", + "direction": "diagonal" + }, + "border_radius": 15, + "shadow": "shadow_soft", + "glow": "glow_subtle" + } + }, + { + "type": "text", + "role": "highlight_2_text", + "position": { + "left": 7.6, + "top": 4.8, + "width": 1.6, + "height": 0.6 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_soft"], + "auto_wrap": true, + "auto_fit": true, + "bold": true + }, + "placeholder_text": "< 1ms\nLatency" + }, + { + "type": "text", + "role": "cta_button", + "position": { + "left": 3.0, + "top": 6.2, + "width": 4.0, + "height": 0.8 + }, + "styling": { + "font_type": "accent", + "font_size": "dynamic", + "alignment": "center", + "color": [255, 255, 255], + "text_effects": ["shadow_strong", "glow_vibrant"], + "auto_wrap": true, + "auto_fit": true, + "bold": true, + "background_shape": { + "type": "rounded_rectangle", + "color_role": "accent2", + "border_radius": 25, + "glow": "glow_vibrant" + }, + "interactive": true, + "hover_effect": "scale_105" + }, + "placeholder_text": "Experience the Future →" + } + ], + "background": { + "type": "product_gradient", + "base_gradient": { + "start_color_role": "light", + "end_color_role": "accent1", + "direction": "radial", + "opacity": 0.1 + }, + "floating_particles": true + } + } + }, + "usage_guide": { + "basic_usage": "Templates can be applied by specifying the template name and color scheme", + "customization": "All elements can be customized including positions, colors, fonts, and content", + "positioning": "All positions are in inches from top-left corner of slide (10\" x 7.5\" standard)", + "color_roles": "Use color_role to automatically apply colors from selected scheme", + "dynamic_features": { + "auto_text_sizing": "Text automatically adjusts based on content length and container size", + "responsive_layouts": "Elements reposition based on content overflow", + "effect_combinations": "Multiple effects can be applied simultaneously", + "gradient_backgrounds": "Advanced gradient backgrounds with patterns and animations" + }, + "element_types": [ + "text - Text content with various formatting options and dynamic sizing", + "image - Pictures with positioning, styling and visual effects", + "shape - Geometric shapes with gradient fills and advanced effects", + "table - Data tables with header and cell formatting", + "chart - Various chart types with data series and styling" + ], + "styling_tips": [ + "Use 'dynamic' font size for automatic text fitting", + "Combine text effects for maximum visual impact", + "Leverage gradient fills for modern appearance", + "Apply hover effects for interactive elements", + "Use animation properties for engaging presentations" + ], + "best_practices": [ + "Use consistent color schemes throughout presentation", + "Maintain proper text hierarchy with title/subtitle/body fonts", + "Leave adequate white space for readability", + "Ensure images are high resolution for professional appearance", + "Test layouts with actual content before finalizing", + "Enable auto-wrapping for content that may vary in length", + "Use dynamic sizing for presentations with variable content" + ] + }, + "implementation_notes": { + "mcp_integration": "Templates designed to work with existing MCP server tools", + "backward_compatibility": "All templates use existing presentation capabilities", + "extensibility": "New templates can be added by following the same JSON structure", + "automation": "Templates can be automatically applied based on content type detection", + "dynamic_features": "Enhanced templates include automatic sizing, wrapping, and effects", + "performance": "Optimized algorithms for real-time text sizing and layout adaptation" + } +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/smithery.yaml new file mode 100644 index 00000000..1025ca49 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/smithery.yaml @@ -0,0 +1,16 @@ +# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml + +startCommand: + type: stdio + configSchema: + # JSON Schema defining the configuration options for the MCP. + {} + commandFunction: + # A JS function that produces the CLI command based on the given config to start the MCP on stdio. + |- + (config) => ({ + command: 'python', + args: ['ppt_mcp_server.py'], + env: {} + }) + exampleConfig: {} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/__init__.py new file mode 100644 index 00000000..c1fb003a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/__init__.py @@ -0,0 +1,28 @@ +""" +Tools package for PowerPoint MCP Server. +Organizes tools into logical modules for better maintainability. +""" + +from .presentation_tools import register_presentation_tools +from .content_tools import register_content_tools +from .structural_tools import register_structural_tools +from .professional_tools import register_professional_tools +from .template_tools import register_template_tools +from .hyperlink_tools import register_hyperlink_tools +from .chart_tools import register_chart_tools +from .connector_tools import register_connector_tools +from .master_tools import register_master_tools +from .transition_tools import register_transition_tools + +__all__ = [ + "register_presentation_tools", + "register_content_tools", + "register_structural_tools", + "register_professional_tools", + "register_template_tools", + "register_hyperlink_tools", + "register_chart_tools", + "register_connector_tools", + "register_master_tools", + "register_transition_tools" +] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/chart_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/chart_tools.py new file mode 100644 index 00000000..0bb98174 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/chart_tools.py @@ -0,0 +1,87 @@ +""" +Chart data management tools for PowerPoint MCP Server. +Implements advanced chart data manipulation capabilities. +""" + +from typing import Dict, List, Optional, Any +from mcp.types import ToolAnnotations +from pptx.chart.data import ChartData + +def register_chart_tools(app, presentations, get_current_presentation_id, validate_parameters, + is_positive, is_non_negative, is_in_range, is_valid_rgb): + """Register chart data management tools with the FastMCP app.""" + + @app.tool( + annotations=ToolAnnotations( + title="Update Chart Data", + ), + ) + def update_chart_data( + slide_index: int, + shape_index: int, + categories: List[str], + series_data: List[Dict], + presentation_id: str = None + ) -> Dict: + """ + Replace existing chart data with new categories and series. + + Args: + slide_index: Index of the slide (0-based) + shape_index: Index of the chart shape (0-based) + categories: List of category names + series_data: List of dictionaries with 'name' and 'values' keys + presentation_id: Optional presentation ID (uses current if not provided) + + Returns: + Dictionary with operation results + """ + try: + # Get presentation + pres_id = presentation_id or get_current_presentation_id() + if pres_id not in presentations: + return {"error": "Presentation not found"} + + pres = presentations[pres_id] + + # Validate slide index + if not (0 <= slide_index < len(pres.slides)): + return {"error": f"Slide index {slide_index} out of range"} + + slide = pres.slides[slide_index] + + # Validate shape index + if not (0 <= shape_index < len(slide.shapes)): + return {"error": f"Shape index {shape_index} out of range"} + + shape = slide.shapes[shape_index] + + # Check if shape is a chart + if not hasattr(shape, 'has_chart') or not shape.has_chart: + return {"error": "Shape is not a chart"} + + chart = shape.chart + + # Create new ChartData + chart_data = ChartData() + chart_data.categories = categories + + # Add series data + for series in series_data: + if 'name' not in series or 'values' not in series: + return {"error": "Each series must have 'name' and 'values' keys"} + + chart_data.add_series(series['name'], series['values']) + + # Replace chart data + chart.replace_data(chart_data) + + return { + "message": f"Updated chart data on slide {slide_index}, shape {shape_index}", + "categories": categories, + "series_count": len(series_data), + "series_names": [s['name'] for s in series_data] + } + + except Exception as e: + return {"error": f"Failed to update chart data: {str(e)}"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/connector_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/connector_tools.py new file mode 100644 index 00000000..4851bc92 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/connector_tools.py @@ -0,0 +1,96 @@ +""" +Connector and line tools for PowerPoint MCP Server. +Implements connector line/arrow drawing capabilities. +""" + +from typing import Dict, List, Optional, Any +from mcp.types import ToolAnnotations +from pptx.util import Inches, Pt +from pptx.enum.shapes import MSO_CONNECTOR +from pptx.dml.color import RGBColor + +def register_connector_tools(app, presentations, get_current_presentation_id, validate_parameters, + is_positive, is_non_negative, is_in_range, is_valid_rgb): + """Register connector tools with the FastMCP app.""" + + @app.tool( + annotations=ToolAnnotations( + title="Add Connector", + ), + ) + def add_connector( + slide_index: int, + connector_type: str, + start_x: float, + start_y: float, + end_x: float, + end_y: float, + line_width: float = 1.0, + color: List[int] = None, + presentation_id: str = None + ) -> Dict: + """ + Add connector lines/arrows between points on a slide. + + Args: + slide_index: Index of the slide (0-based) + connector_type: Type of connector ("straight", "elbow", "curved") + start_x: Starting X coordinate in inches + start_y: Starting Y coordinate in inches + end_x: Ending X coordinate in inches + end_y: Ending Y coordinate in inches + line_width: Width of the connector line in points + color: RGB color as [r, g, b] list + presentation_id: Optional presentation ID (uses current if not provided) + + Returns: + Dictionary with operation results + """ + try: + # Get presentation + pres_id = presentation_id or get_current_presentation_id() + if pres_id not in presentations: + return {"error": "Presentation not found"} + + pres = presentations[pres_id] + + # Validate slide index + if not (0 <= slide_index < len(pres.slides)): + return {"error": f"Slide index {slide_index} out of range"} + + slide = pres.slides[slide_index] + + # Map connector types + connector_map = { + 'straight': MSO_CONNECTOR.STRAIGHT, + 'elbow': MSO_CONNECTOR.ELBOW, + 'curved': MSO_CONNECTOR.CURVED + } + + if connector_type.lower() not in connector_map: + return {"error": f"Invalid connector type. Use: {list(connector_map.keys())}"} + + # Add connector + connector = slide.shapes.add_connector( + connector_map[connector_type.lower()], + Inches(start_x), Inches(start_y), + Inches(end_x), Inches(end_y) + ) + + # Apply formatting + if line_width: + connector.line.width = Pt(line_width) + + if color and is_valid_rgb(color): + connector.line.color.rgb = RGBColor(*color) + + return { + "message": f"Added {connector_type} connector to slide {slide_index}", + "connector_type": connector_type, + "start_point": [start_x, start_y], + "end_point": [end_x, end_y], + "shape_index": len(slide.shapes) - 1 + } + + except Exception as e: + return {"error": f"Failed to add connector: {str(e)}"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/content_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/content_tools.py new file mode 100644 index 00000000..3c25e59b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/content_tools.py @@ -0,0 +1,629 @@ +""" +Content management tools for PowerPoint MCP Server. +Handles slides, text, images, and content manipulation. +""" +from typing import Dict, List, Optional, Any, Union +from mcp.server.fastmcp import FastMCP +from mcp.types import ToolAnnotations +import utils as ppt_utils +import tempfile +import base64 +import os + + +def register_content_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb): + """Register content management tools with the FastMCP app""" + + @app.tool( + annotations=ToolAnnotations( + title="Add Slide", + ), + ) + def add_slide( + layout_index: int = 1, + title: Optional[str] = None, + background_type: Optional[str] = None, # "solid", "gradient", "professional_gradient" + background_colors: Optional[List[List[int]]] = None, # For gradient: [[start_rgb], [end_rgb]] + gradient_direction: str = "horizontal", + color_scheme: str = "modern_blue", + presentation_id: Optional[str] = None + ) -> Dict: + """Add a new slide to the presentation with optional background styling.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + # Validate layout index + if layout_index < 0 or layout_index >= len(pres.slide_layouts): + return { + "error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}" + } + + try: + # Add the slide + slide, layout = ppt_utils.add_slide(pres, layout_index) + slide_index = len(pres.slides) - 1 + + # Set title if provided + if title: + ppt_utils.set_title(slide, title) + + # Apply background if specified + if background_type == "gradient" and background_colors and len(background_colors) >= 2: + ppt_utils.set_slide_gradient_background( + slide, background_colors[0], background_colors[1], gradient_direction + ) + elif background_type == "professional_gradient": + ppt_utils.create_professional_gradient_background( + slide, color_scheme, "subtle", gradient_direction + ) + + return { + "message": f"Added slide {slide_index} with layout {layout_index}", + "slide_index": slide_index, + "layout_name": layout.name if hasattr(layout, 'name') else f"Layout {layout_index}" + } + except Exception as e: + return { + "error": f"Failed to add slide: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Get Slide Info", + readOnlyHint=True, + ), + ) + def get_slide_info(slide_index: int, presentation_id: Optional[str] = None) -> Dict: + """Get information about a specific slide.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + return ppt_utils.get_slide_info(slide, slide_index) + except Exception as e: + return { + "error": f"Failed to get slide info: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Extract Slide Text", + readOnlyHint=True, + ), + ) + def extract_slide_text(slide_index: int, presentation_id: Optional[str] = None) -> Dict: + """Extract all text content from a specific slide.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + result = ppt_utils.extract_slide_text_content(slide) + result["slide_index"] = slide_index + return result + except Exception as e: + return { + "error": f"Failed to extract slide text: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Extract Presentation Text", + readOnlyHint=True, + ), + ) + def extract_presentation_text(presentation_id: Optional[str] = None, include_slide_info: bool = True) -> Dict: + """Extract all text content from all slides in the presentation.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + try: + slides_text = [] + total_text_shapes = 0 + slides_with_tables = 0 + slides_with_titles = 0 + all_presentation_text = [] + + for slide_index, slide in enumerate(pres.slides): + slide_text_result = ppt_utils.extract_slide_text_content(slide) + + if slide_text_result["success"]: + slide_data = { + "slide_index": slide_index, + "text_content": slide_text_result["text_content"] + } + + if include_slide_info: + # Add basic slide info + slide_data["layout_name"] = slide.slide_layout.name + slide_data["total_text_shapes"] = slide_text_result["total_text_shapes"] + slide_data["has_title"] = slide_text_result["has_title"] + slide_data["has_tables"] = slide_text_result["has_tables"] + + slides_text.append(slide_data) + + # Accumulate statistics + total_text_shapes += slide_text_result["total_text_shapes"] + if slide_text_result["has_tables"]: + slides_with_tables += 1 + if slide_text_result["has_title"]: + slides_with_titles += 1 + + # Collect all text for combined output + if slide_text_result["text_content"]["all_text_combined"]: + all_presentation_text.append(f"=== SLIDE {slide_index + 1} ===") + all_presentation_text.append(slide_text_result["text_content"]["all_text_combined"]) + all_presentation_text.append("") # Empty line separator + else: + slides_text.append({ + "slide_index": slide_index, + "error": slide_text_result.get("error", "Unknown error"), + "text_content": None + }) + + return { + "success": True, + "presentation_id": pres_id, + "total_slides": len(pres.slides), + "slides_with_text": len([s for s in slides_text if s.get("text_content") is not None]), + "total_text_shapes": total_text_shapes, + "slides_with_titles": slides_with_titles, + "slides_with_tables": slides_with_tables, + "slides_text": slides_text, + "all_presentation_text_combined": "\n".join(all_presentation_text) + } + + except Exception as e: + return { + "error": f"Failed to extract presentation text: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Populate Placeholder", + ), + ) + def populate_placeholder( + slide_index: int, + placeholder_idx: int, + text: str, + presentation_id: Optional[str] = None + ) -> Dict: + """Populate a placeholder with text.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + ppt_utils.populate_placeholder(slide, placeholder_idx, text) + return { + "message": f"Populated placeholder {placeholder_idx} on slide {slide_index}" + } + except Exception as e: + return { + "error": f"Failed to populate placeholder: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Add Bullet Points", + ), + ) + def add_bullet_points( + slide_index: int, + placeholder_idx: int, + bullet_points: List[str], + presentation_id: Optional[str] = None + ) -> Dict: + """Add bullet points to a placeholder.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + placeholder = slide.placeholders[placeholder_idx] + ppt_utils.add_bullet_points(placeholder, bullet_points) + return { + "message": f"Added {len(bullet_points)} bullet points to placeholder {placeholder_idx} on slide {slide_index}" + } + except Exception as e: + return { + "error": f"Failed to add bullet points: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Manage Text", + ), + ) + def manage_text( + slide_index: int, + operation: str, # "add", "format", "validate", "format_runs" + left: float = 1.0, + top: float = 1.0, + width: float = 4.0, + height: float = 2.0, + text: str = "", + shape_index: Optional[int] = None, # For format/validate operations + text_runs: Optional[List[Dict]] = None, # For format_runs operation + # Formatting options + font_size: Optional[int] = None, + font_name: Optional[str] = None, + bold: Optional[bool] = None, + italic: Optional[bool] = None, + underline: Optional[bool] = None, + color: Optional[List[int]] = None, + bg_color: Optional[List[int]] = None, + alignment: Optional[str] = None, + vertical_alignment: Optional[str] = None, + # Advanced options + auto_fit: bool = True, + validation_only: bool = False, + min_font_size: int = 8, + max_font_size: int = 72, + presentation_id: Optional[str] = None + ) -> Dict: + """Unified text management tool for adding, formatting, validating text, and formatting multiple text runs.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + # Validate parameters + validations = {} + if font_size is not None: + validations["font_size"] = (font_size, [(is_positive, "must be a positive integer")]) + if color is not None: + validations["color"] = (color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) + if bg_color is not None: + validations["bg_color"] = (bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) + + if validations: + valid, error = validate_parameters(validations) + if not valid: + return {"error": error} + + try: + if operation == "add": + # Add new textbox + shape = ppt_utils.add_textbox( + slide, left, top, width, height, text, + font_size=font_size, + font_name=font_name, + bold=bold, + italic=italic, + underline=underline, + color=tuple(color) if color else None, + bg_color=tuple(bg_color) if bg_color else None, + alignment=alignment, + vertical_alignment=vertical_alignment, + auto_fit=auto_fit + ) + return { + "message": f"Added text box to slide {slide_index}", + "shape_index": len(slide.shapes) - 1, + "text": text + } + + elif operation == "format": + # Format existing text shape + if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes): + return { + "error": f"Invalid shape index for formatting: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" + } + + shape = slide.shapes[shape_index] + ppt_utils.format_text_advanced( + shape, + font_size=font_size, + font_name=font_name, + bold=bold, + italic=italic, + underline=underline, + color=tuple(color) if color else None, + bg_color=tuple(bg_color) if bg_color else None, + alignment=alignment, + vertical_alignment=vertical_alignment + ) + return { + "message": f"Formatted text shape {shape_index} on slide {slide_index}" + } + + elif operation == "validate": + # Validate text fit + if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes): + return { + "error": f"Invalid shape index for validation: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" + } + + validation_result = ppt_utils.validate_text_fit( + slide.shapes[shape_index], + text_content=text or None, + font_size=font_size or 12 + ) + + if not validation_only and validation_result.get("needs_optimization"): + # Apply automatic fixes + fix_result = ppt_utils.validate_and_fix_slide( + slide, + auto_fix=True, + min_font_size=min_font_size, + max_font_size=max_font_size + ) + validation_result.update(fix_result) + + return validation_result + + elif operation == "format_runs": + # Format multiple text runs with different formatting + if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes): + return { + "error": f"Invalid shape index for format_runs: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" + } + + if not text_runs: + return {"error": "text_runs parameter is required for format_runs operation"} + + shape = slide.shapes[shape_index] + + # Check if shape has text + if not hasattr(shape, 'text_frame') or not shape.text_frame: + return {"error": "Shape does not contain text"} + + # Clear existing text and rebuild with formatted runs + text_frame = shape.text_frame + text_frame.clear() + + formatted_runs = [] + + for run_data in text_runs: + if 'text' not in run_data: + continue + + # Add paragraph if needed + if not text_frame.paragraphs: + paragraph = text_frame.paragraphs[0] + else: + paragraph = text_frame.add_paragraph() + + # Add run with text + run = paragraph.add_run() + run.text = run_data['text'] + + # Apply formatting using pptx imports + from pptx.util import Pt + from pptx.dml.color import RGBColor + + if 'bold' in run_data: + run.font.bold = run_data['bold'] + if 'italic' in run_data: + run.font.italic = run_data['italic'] + if 'underline' in run_data: + run.font.underline = run_data['underline'] + if 'font_size' in run_data: + run.font.size = Pt(run_data['font_size']) + if 'font_name' in run_data: + run.font.name = run_data['font_name'] + if 'color' in run_data and is_valid_rgb(run_data['color']): + run.font.color.rgb = RGBColor(*run_data['color']) + if 'hyperlink' in run_data: + run.hyperlink.address = run_data['hyperlink'] + + formatted_runs.append({ + "text": run_data['text'], + "formatting_applied": {k: v for k, v in run_data.items() if k != 'text'} + }) + + return { + "message": f"Applied formatting to {len(formatted_runs)} text runs on shape {shape_index}", + "slide_index": slide_index, + "shape_index": shape_index, + "formatted_runs": formatted_runs + } + + else: + return { + "error": f"Invalid operation: {operation}. Must be 'add', 'format', 'validate', or 'format_runs'" + } + + except Exception as e: + return { + "error": f"Failed to {operation} text: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Manage Image", + ), + ) + def manage_image( + slide_index: int, + operation: str, # "add", "enhance" + image_source: str, # file path or base64 string + source_type: str = "file", # "file" or "base64" + left: float = 1.0, + top: float = 1.0, + width: Optional[float] = None, + height: Optional[float] = None, + # Enhancement options + enhancement_style: Optional[str] = None, # "presentation", "custom" + brightness: float = 1.0, + contrast: float = 1.0, + saturation: float = 1.0, + sharpness: float = 1.0, + blur_radius: float = 0, + filter_type: Optional[str] = None, + output_path: Optional[str] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """Unified image management tool for adding and enhancing images.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + if operation == "add": + if source_type == "base64": + # Handle base64 image + try: + image_data = base64.b64decode(image_source) + with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file: + temp_file.write(image_data) + temp_path = temp_file.name + + # Add image from temporary file + shape = ppt_utils.add_image(slide, temp_path, left, top, width, height) + + # Clean up temporary file + os.unlink(temp_path) + + return { + "message": f"Added image from base64 to slide {slide_index}", + "shape_index": len(slide.shapes) - 1 + } + except Exception as e: + return { + "error": f"Failed to process base64 image: {str(e)}" + } + else: + # Handle file path + if not os.path.exists(image_source): + return { + "error": f"Image file not found: {image_source}" + } + + shape = ppt_utils.add_image(slide, image_source, left, top, width, height) + return { + "message": f"Added image to slide {slide_index}", + "shape_index": len(slide.shapes) - 1, + "image_path": image_source + } + + elif operation == "enhance": + # Enhance existing image file + if source_type == "base64": + return { + "error": "Enhancement operation requires file path, not base64 data" + } + + if not os.path.exists(image_source): + return { + "error": f"Image file not found: {image_source}" + } + + if enhancement_style == "presentation": + # Apply professional enhancement + enhanced_path = ppt_utils.apply_professional_image_enhancement( + image_source, style="presentation", output_path=output_path + ) + else: + # Apply custom enhancement + enhanced_path = ppt_utils.enhance_image_with_pillow( + image_source, + brightness=brightness, + contrast=contrast, + saturation=saturation, + sharpness=sharpness, + blur_radius=blur_radius, + filter_type=filter_type, + output_path=output_path + ) + + return { + "message": f"Enhanced image: {image_source}", + "enhanced_path": enhanced_path + } + + else: + return { + "error": f"Invalid operation: {operation}. Must be 'add' or 'enhance'" + } + + except Exception as e: + return { + "error": f"Failed to {operation} image: {str(e)}" + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/hyperlink_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/hyperlink_tools.py new file mode 100644 index 00000000..fd045377 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/hyperlink_tools.py @@ -0,0 +1,143 @@ +""" +Hyperlink management tools for PowerPoint MCP Server. +Implements hyperlink operations for text shapes and runs. +""" + +from typing import Dict, List, Optional, Any +from mcp.types import ToolAnnotations + +def register_hyperlink_tools(app, presentations, get_current_presentation_id, validate_parameters, + is_positive, is_non_negative, is_in_range, is_valid_rgb): + """Register hyperlink management tools with the FastMCP app.""" + + @app.tool( + annotations=ToolAnnotations( + title="Manage Hyperlinks", + ), + ) + def manage_hyperlinks( + operation: str, + slide_index: int, + shape_index: int = None, + text: str = None, + url: str = None, + run_index: int = 0, + presentation_id: str = None + ) -> Dict: + """ + Manage hyperlinks in text shapes and runs. + + Args: + operation: Operation type ("add", "remove", "list", "update") + slide_index: Index of the slide (0-based) + shape_index: Index of the shape on the slide (0-based) + text: Text to make into hyperlink (for "add" operation) + url: URL for the hyperlink + run_index: Index of text run within the shape (0-based) + presentation_id: Optional presentation ID (uses current if not provided) + + Returns: + Dictionary with operation results + """ + try: + # Get presentation + pres_id = presentation_id or get_current_presentation_id() + if pres_id not in presentations: + return {"error": "Presentation not found"} + + pres = presentations[pres_id] + + # Validate slide index + if not (0 <= slide_index < len(pres.slides)): + return {"error": f"Slide index {slide_index} out of range"} + + slide = pres.slides[slide_index] + + if operation == "list": + # List all hyperlinks in the slide + hyperlinks = [] + for shape_idx, shape in enumerate(slide.shapes): + if hasattr(shape, 'text_frame') and shape.text_frame: + for para_idx, paragraph in enumerate(shape.text_frame.paragraphs): + for run_idx, run in enumerate(paragraph.runs): + if run.hyperlink.address: + hyperlinks.append({ + "shape_index": shape_idx, + "paragraph_index": para_idx, + "run_index": run_idx, + "text": run.text, + "url": run.hyperlink.address + }) + + return { + "message": f"Found {len(hyperlinks)} hyperlinks on slide {slide_index}", + "hyperlinks": hyperlinks + } + + # For other operations, validate shape index + if shape_index is None or not (0 <= shape_index < len(slide.shapes)): + return {"error": f"Shape index {shape_index} out of range"} + + shape = slide.shapes[shape_index] + + # Check if shape has text + if not hasattr(shape, 'text_frame') or not shape.text_frame: + return {"error": "Shape does not contain text"} + + if operation == "add": + if not text or not url: + return {"error": "Both 'text' and 'url' are required for adding hyperlinks"} + + # Add new text run with hyperlink + paragraph = shape.text_frame.paragraphs[0] + run = paragraph.add_run() + run.text = text + run.hyperlink.address = url + + return { + "message": f"Added hyperlink '{text}' -> '{url}' to shape {shape_index}", + "text": text, + "url": url + } + + elif operation == "update": + if not url: + return {"error": "URL is required for updating hyperlinks"} + + # Update existing hyperlink + paragraphs = shape.text_frame.paragraphs + if run_index < len(paragraphs[0].runs): + run = paragraphs[0].runs[run_index] + old_url = run.hyperlink.address + run.hyperlink.address = url + + return { + "message": f"Updated hyperlink from '{old_url}' to '{url}'", + "old_url": old_url, + "new_url": url, + "text": run.text + } + else: + return {"error": f"Run index {run_index} out of range"} + + elif operation == "remove": + # Remove hyperlink from specific run + paragraphs = shape.text_frame.paragraphs + if run_index < len(paragraphs[0].runs): + run = paragraphs[0].runs[run_index] + old_url = run.hyperlink.address + run.hyperlink.address = None + + return { + "message": f"Removed hyperlink '{old_url}' from text '{run.text}'", + "removed_url": old_url, + "text": run.text + } + else: + return {"error": f"Run index {run_index} out of range"} + + else: + return {"error": f"Unsupported operation: {operation}. Use 'add', 'remove', 'list', or 'update'"} + + except Exception as e: + return {"error": f"Failed to manage hyperlinks: {str(e)}"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/master_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/master_tools.py new file mode 100644 index 00000000..b2446177 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/master_tools.py @@ -0,0 +1,119 @@ +""" +Slide master management tools for PowerPoint MCP Server. +Implements slide master and layout access capabilities. +""" + +from typing import Dict, List, Optional, Any +from mcp.types import ToolAnnotations + +def register_master_tools(app, presentations, get_current_presentation_id, validate_parameters, + is_positive, is_non_negative, is_in_range, is_valid_rgb): + """Register slide master management tools with the FastMCP app.""" + + @app.tool( + annotations=ToolAnnotations( + title="Manage Slide Masters", + ), + ) + def manage_slide_masters( + operation: str, + master_index: int = 0, + layout_index: int = None, + presentation_id: str = None + ) -> Dict: + """ + Access and manage slide master properties and layouts. + + Args: + operation: Operation type ("list", "get_layouts", "get_info") + master_index: Index of the slide master (0-based) + layout_index: Index of specific layout within master (0-based) + presentation_id: Optional presentation ID (uses current if not provided) + + Returns: + Dictionary with slide master information + """ + try: + # Get presentation + pres_id = presentation_id or get_current_presentation_id() + if pres_id not in presentations: + return {"error": "Presentation not found"} + + pres = presentations[pres_id] + + if operation == "list": + # List all slide masters + masters_info = [] + for idx, master in enumerate(pres.slide_masters): + masters_info.append({ + "index": idx, + "layout_count": len(master.slide_layouts), + "name": getattr(master, 'name', f"Master {idx}") + }) + + return { + "message": f"Found {len(masters_info)} slide masters", + "masters": masters_info, + "total_masters": len(pres.slide_masters) + } + + # Validate master index + if not (0 <= master_index < len(pres.slide_masters)): + return {"error": f"Master index {master_index} out of range"} + + master = pres.slide_masters[master_index] + + if operation == "get_layouts": + # Get all layouts for a specific master + layouts_info = [] + for idx, layout in enumerate(master.slide_layouts): + layouts_info.append({ + "index": idx, + "name": layout.name, + "placeholder_count": len(layout.placeholders) if hasattr(layout, 'placeholders') else 0 + }) + + return { + "message": f"Master {master_index} has {len(layouts_info)} layouts", + "master_index": master_index, + "layouts": layouts_info + } + + elif operation == "get_info": + # Get detailed info about master or specific layout + if layout_index is not None: + if not (0 <= layout_index < len(master.slide_layouts)): + return {"error": f"Layout index {layout_index} out of range"} + + layout = master.slide_layouts[layout_index] + placeholders_info = [] + + if hasattr(layout, 'placeholders'): + for placeholder in layout.placeholders: + placeholders_info.append({ + "idx": placeholder.placeholder_format.idx, + "type": str(placeholder.placeholder_format.type), + "name": getattr(placeholder, 'name', 'Unnamed') + }) + + return { + "message": f"Layout info for master {master_index}, layout {layout_index}", + "master_index": master_index, + "layout_index": layout_index, + "layout_name": layout.name, + "placeholders": placeholders_info + } + else: + # Master info + return { + "message": f"Master {master_index} information", + "master_index": master_index, + "layout_count": len(master.slide_layouts), + "name": getattr(master, 'name', f"Master {master_index}") + } + + else: + return {"error": f"Unsupported operation: {operation}. Use 'list', 'get_layouts', or 'get_info'"} + + except Exception as e: + return {"error": f"Failed to manage slide masters: {str(e)}"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/presentation_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/presentation_tools.py new file mode 100644 index 00000000..2f1ee5c7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/presentation_tools.py @@ -0,0 +1,245 @@ +""" +Presentation management tools for PowerPoint MCP Server. +Handles presentation creation, opening, saving, and core properties. +""" +from typing import Dict, List, Optional, Any +import os +from mcp.server.fastmcp import FastMCP +from mcp.types import ToolAnnotations +import utils as ppt_utils + + +def register_presentation_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, get_template_search_directories): + """Register presentation management tools with the FastMCP app""" + + @app.tool( + annotations=ToolAnnotations( + title="Create Presentation", + ), + ) + def create_presentation(id: Optional[str] = None) -> Dict: + """Create a new PowerPoint presentation.""" + # Create a new presentation + pres = ppt_utils.create_presentation() + + # Generate an ID if not provided + if id is None: + id = f"presentation_{len(presentations) + 1}" + + # Store the presentation + presentations[id] = pres + # Set as current presentation (this would need to be handled by caller) + + return { + "presentation_id": id, + "message": f"Created new presentation with ID: {id}", + "slide_count": len(pres.slides) + } + + @app.tool( + annotations=ToolAnnotations( + title="Create Presentation from Template", + ), + ) + def create_presentation_from_template(template_path: str, id: Optional[str] = None) -> Dict: + """Create a new PowerPoint presentation from a template file.""" + # Check if template file exists + if not os.path.exists(template_path): + # Try to find the template by searching in configured directories + search_dirs = get_template_search_directories() + template_name = os.path.basename(template_path) + + for directory in search_dirs: + potential_path = os.path.join(directory, template_name) + if os.path.exists(potential_path): + template_path = potential_path + break + else: + env_path_info = f" (PPT_TEMPLATE_PATH: {os.environ.get('PPT_TEMPLATE_PATH', 'not set')})" if os.environ.get('PPT_TEMPLATE_PATH') else "" + return { + "error": f"Template file not found: {template_path}. Searched in {', '.join(search_dirs)}{env_path_info}" + } + + # Create presentation from template + try: + pres = ppt_utils.create_presentation_from_template(template_path) + except Exception as e: + return { + "error": f"Failed to create presentation from template: {str(e)}" + } + + # Generate an ID if not provided + if id is None: + id = f"presentation_{len(presentations) + 1}" + + # Store the presentation + presentations[id] = pres + + return { + "presentation_id": id, + "message": f"Created new presentation from template '{template_path}' with ID: {id}", + "template_path": template_path, + "slide_count": len(pres.slides), + "layout_count": len(pres.slide_layouts) + } + + @app.tool( + annotations=ToolAnnotations( + title="Open Presentation", + readOnlyHint=True, + ), + ) + def open_presentation(file_path: str, id: Optional[str] = None) -> Dict: + """Open an existing PowerPoint presentation from a file.""" + # Check if file exists + if not os.path.exists(file_path): + return { + "error": f"File not found: {file_path}" + } + + # Open the presentation + try: + pres = ppt_utils.open_presentation(file_path) + except Exception as e: + return { + "error": f"Failed to open presentation: {str(e)}" + } + + # Generate an ID if not provided + if id is None: + id = f"presentation_{len(presentations) + 1}" + + # Store the presentation + presentations[id] = pres + + return { + "presentation_id": id, + "message": f"Opened presentation from {file_path} with ID: {id}", + "slide_count": len(pres.slides) + } + + @app.tool( + annotations=ToolAnnotations( + title="Save Presentation", + destructiveHint=True, + ), + ) + def save_presentation(file_path: str, presentation_id: Optional[str] = None) -> Dict: + """Save a presentation to a file.""" + # Use the specified presentation or the current one + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + # Save the presentation + try: + saved_path = ppt_utils.save_presentation(presentations[pres_id], file_path) + return { + "message": f"Presentation saved to {saved_path}", + "file_path": saved_path + } + except Exception as e: + return { + "error": f"Failed to save presentation: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Get Presentation Info", + readOnlyHint=True, + ), + ) + def get_presentation_info(presentation_id: Optional[str] = None) -> Dict: + """Get information about a presentation.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + try: + info = ppt_utils.get_presentation_info(pres) + info["presentation_id"] = pres_id + return info + except Exception as e: + return { + "error": f"Failed to get presentation info: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Get Template File Info", + readOnlyHint=True, + ), + ) + def get_template_file_info(template_path: str) -> Dict: + """Get information about a template file including layouts and properties.""" + # Check if template file exists + if not os.path.exists(template_path): + # Try to find the template by searching in configured directories + search_dirs = get_template_search_directories() + template_name = os.path.basename(template_path) + + for directory in search_dirs: + potential_path = os.path.join(directory, template_name) + if os.path.exists(potential_path): + template_path = potential_path + break + else: + return { + "error": f"Template file not found: {template_path}. Searched in {', '.join(search_dirs)}" + } + + try: + return ppt_utils.get_template_info(template_path) + except Exception as e: + return { + "error": f"Failed to get template info: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Set Core Properties", + ), + ) + def set_core_properties( + title: Optional[str] = None, + subject: Optional[str] = None, + author: Optional[str] = None, + keywords: Optional[str] = None, + comments: Optional[str] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """Set core document properties.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + try: + ppt_utils.set_core_properties( + pres, + title=title, + subject=subject, + author=author, + keywords=keywords, + comments=comments + ) + + return { + "message": "Core properties updated successfully" + } + except Exception as e: + return { + "error": f"Failed to set core properties: {str(e)}" + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/professional_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/professional_tools.py new file mode 100644 index 00000000..d9ef70b0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/professional_tools.py @@ -0,0 +1,303 @@ +""" +Professional design tools for PowerPoint MCP Server. +Handles themes, effects, fonts, and advanced formatting. +""" +from typing import Dict, List, Optional, Any +from mcp.server.fastmcp import FastMCP +from mcp.types import ToolAnnotations +import utils as ppt_utils + + +def register_professional_tools(app: FastMCP, presentations: Dict, get_current_presentation_id): + """Register professional design tools with the FastMCP app""" + + @app.tool( + annotations=ToolAnnotations( + title="Apply Professional Design", + ), + ) + def apply_professional_design( + operation: str, # "professional_slide", "theme", "enhance", "get_schemes" + slide_index: Optional[int] = None, + slide_type: str = "title_content", + color_scheme: str = "modern_blue", + title: Optional[str] = None, + content: Optional[List[str]] = None, + apply_to_existing: bool = True, + enhance_title: bool = True, + enhance_content: bool = True, + enhance_shapes: bool = True, + enhance_charts: bool = True, + presentation_id: Optional[str] = None + ) -> Dict: + """Unified professional design tool for themes, slides, and visual enhancements. + This applies professional styling and themes rather than structural layout changes.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if operation == "get_schemes": + # Return available color schemes + return ppt_utils.get_color_schemes() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + try: + if operation == "professional_slide": + # Add professional slide with advanced styling + if slide_index is not None and (slide_index < 0 or slide_index >= len(pres.slides)): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + result = ppt_utils.add_professional_slide( + pres, + slide_type=slide_type, + color_scheme=color_scheme, + title=title, + content=content + ) + + return { + "message": f"Added professional {slide_type} slide", + "slide_index": len(pres.slides) - 1, + "color_scheme": color_scheme, + "slide_type": slide_type + } + + elif operation == "theme": + # Apply professional theme + ppt_utils.apply_professional_theme( + pres, + color_scheme=color_scheme, + apply_to_existing=apply_to_existing + ) + + return { + "message": f"Applied {color_scheme} theme to presentation", + "color_scheme": color_scheme, + "applied_to_existing": apply_to_existing + } + + elif operation == "enhance": + # Enhance existing slide + if slide_index is None: + return { + "error": "slide_index is required for enhance operation" + } + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + result = ppt_utils.enhance_existing_slide( + slide, + color_scheme=color_scheme, + enhance_title=enhance_title, + enhance_content=enhance_content, + enhance_shapes=enhance_shapes, + enhance_charts=enhance_charts + ) + + return { + "message": f"Enhanced slide {slide_index} with {color_scheme} scheme", + "slide_index": slide_index, + "color_scheme": color_scheme, + "enhancements_applied": result.get("enhancements_applied", []) + } + + else: + return { + "error": f"Invalid operation: {operation}. Must be 'slide', 'theme', 'enhance', or 'get_schemes'" + } + + except Exception as e: + return { + "error": f"Failed to apply professional design: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Apply Picture Effects", + ), + ) + def apply_picture_effects( + slide_index: int, + shape_index: int, + effects: Dict[str, Dict], # {"shadow": {"blur_radius": 4.0, ...}, "glow": {...}} + presentation_id: Optional[str] = None + ) -> Dict: + """Apply multiple picture effects in combination.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + if shape_index < 0 or shape_index >= len(slide.shapes): + return { + "error": f"Invalid shape index: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" + } + + shape = slide.shapes[shape_index] + + try: + applied_effects = [] + warnings = [] + + # Apply each effect + for effect_type, effect_params in effects.items(): + try: + if effect_type == "shadow": + ppt_utils.apply_picture_shadow( + shape, + shadow_type=effect_params.get("shadow_type", "outer"), + blur_radius=effect_params.get("blur_radius", 4.0), + distance=effect_params.get("distance", 3.0), + direction=effect_params.get("direction", 315.0), + color=effect_params.get("color", [0, 0, 0]), + transparency=effect_params.get("transparency", 0.6) + ) + applied_effects.append("shadow") + + elif effect_type == "reflection": + ppt_utils.apply_picture_reflection( + shape, + size=effect_params.get("size", 0.5), + transparency=effect_params.get("transparency", 0.5), + distance=effect_params.get("distance", 0.0), + blur=effect_params.get("blur", 4.0) + ) + applied_effects.append("reflection") + + elif effect_type == "glow": + ppt_utils.apply_picture_glow( + shape, + size=effect_params.get("size", 5.0), + color=effect_params.get("color", [0, 176, 240]), + transparency=effect_params.get("transparency", 0.4) + ) + applied_effects.append("glow") + + elif effect_type == "soft_edges": + ppt_utils.apply_picture_soft_edges( + shape, + radius=effect_params.get("radius", 2.5) + ) + applied_effects.append("soft_edges") + + elif effect_type == "rotation": + ppt_utils.apply_picture_rotation( + shape, + rotation=effect_params.get("rotation", 0.0) + ) + applied_effects.append("rotation") + + elif effect_type == "transparency": + ppt_utils.apply_picture_transparency( + shape, + transparency=effect_params.get("transparency", 0.0) + ) + applied_effects.append("transparency") + + elif effect_type == "bevel": + ppt_utils.apply_picture_bevel( + shape, + bevel_type=effect_params.get("bevel_type", "circle"), + width=effect_params.get("width", 6.0), + height=effect_params.get("height", 6.0) + ) + applied_effects.append("bevel") + + elif effect_type == "filter": + ppt_utils.apply_picture_filter( + shape, + filter_type=effect_params.get("filter_type", "none"), + intensity=effect_params.get("intensity", 0.5) + ) + applied_effects.append("filter") + + else: + warnings.append(f"Unknown effect type: {effect_type}") + + except Exception as e: + warnings.append(f"Failed to apply {effect_type} effect: {str(e)}") + + result = { + "message": f"Applied {len(applied_effects)} effects to shape {shape_index} on slide {slide_index}", + "applied_effects": applied_effects + } + + if warnings: + result["warnings"] = warnings + + return result + + except Exception as e: + return { + "error": f"Failed to apply picture effects: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Manage Fonts", + ), + ) + def manage_fonts( + operation: str, # "analyze", "optimize", "recommend" + font_path: str, + output_path: Optional[str] = None, + presentation_type: str = "business", + text_content: Optional[str] = None + ) -> Dict: + """Unified font management tool for analysis, optimization, and recommendations.""" + try: + if operation == "analyze": + # Analyze font file + return ppt_utils.analyze_font_file(font_path) + + elif operation == "optimize": + # Optimize font file + optimized_path = ppt_utils.optimize_font_for_presentation( + font_path, + output_path=output_path, + text_content=text_content + ) + + return { + "message": f"Optimized font: {font_path}", + "original_path": font_path, + "optimized_path": optimized_path + } + + elif operation == "recommend": + # Get font recommendations + return ppt_utils.get_font_recommendations( + font_path, + presentation_type=presentation_type + ) + + else: + return { + "error": f"Invalid operation: {operation}. Must be 'analyze', 'optimize', or 'recommend'" + } + + except Exception as e: + return { + "error": f"Failed to {operation} font: {str(e)}" + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/structural_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/structural_tools.py new file mode 100644 index 00000000..c1489efe --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/structural_tools.py @@ -0,0 +1,390 @@ +""" +Structural element tools for PowerPoint MCP Server. +Handles tables, shapes, and charts. +""" +from typing import Dict, List, Optional, Any +from mcp.server.fastmcp import FastMCP +from mcp.types import ToolAnnotations +import utils as ppt_utils + + +def register_structural_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb, add_shape_direct): + """Register structural element tools with the FastMCP app""" + + @app.tool( + annotations=ToolAnnotations( + title="Add Table", + ), + ) + def add_table( + slide_index: int, + rows: int, + cols: int, + left: float, + top: float, + width: float, + height: float, + data: Optional[List[List[str]]] = None, + header_row: bool = True, + header_font_size: int = 12, + body_font_size: int = 10, + header_bg_color: Optional[List[int]] = None, + body_bg_color: Optional[List[int]] = None, + border_color: Optional[List[int]] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """Add a table to a slide with enhanced formatting options.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + # Validate parameters + validations = { + "rows": (rows, [(is_positive, "must be a positive integer")]), + "cols": (cols, [(is_positive, "must be a positive integer")]), + "left": (left, [(is_non_negative, "must be non-negative")]), + "top": (top, [(is_non_negative, "must be non-negative")]), + "width": (width, [(is_positive, "must be positive")]), + "height": (height, [(is_positive, "must be positive")]) + } + + if header_bg_color is not None: + validations["header_bg_color"] = (header_bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) + if body_bg_color is not None: + validations["body_bg_color"] = (body_bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) + if border_color is not None: + validations["border_color"] = (border_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")]) + + valid, error = validate_parameters(validations) + if not valid: + return {"error": error} + + # Validate data if provided + if data: + if len(data) != rows: + return { + "error": f"Data has {len(data)} rows but table should have {rows} rows" + } + for i, row in enumerate(data): + if len(row) != cols: + return { + "error": f"Row {i} has {len(row)} columns but table should have {cols} columns" + } + + try: + # Add the table + table_shape = ppt_utils.add_table(slide, rows, cols, left, top, width, height) + table = table_shape.table + + # Populate with data if provided + if data: + for r in range(rows): + for c in range(cols): + if r < len(data) and c < len(data[r]): + table.cell(r, c).text = str(data[r][c]) + + # Apply formatting + for r in range(rows): + for c in range(cols): + cell = table.cell(r, c) + + # Header row formatting + if r == 0 and header_row: + if header_bg_color: + ppt_utils.format_table_cell( + cell, bg_color=tuple(header_bg_color), font_size=header_font_size, bold=True + ) + else: + ppt_utils.format_table_cell(cell, font_size=header_font_size, bold=True) + else: + # Body cell formatting + if body_bg_color: + ppt_utils.format_table_cell( + cell, bg_color=tuple(body_bg_color), font_size=body_font_size + ) + else: + ppt_utils.format_table_cell(cell, font_size=body_font_size) + + return { + "message": f"Added {rows}x{cols} table to slide {slide_index}", + "shape_index": len(slide.shapes) - 1, + "rows": rows, + "cols": cols + } + except Exception as e: + return { + "error": f"Failed to add table: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Format Table Cell", + ), + ) + def format_table_cell( + slide_index: int, + shape_index: int, + row: int, + col: int, + font_size: Optional[int] = None, + font_name: Optional[str] = None, + bold: Optional[bool] = None, + italic: Optional[bool] = None, + color: Optional[List[int]] = None, + bg_color: Optional[List[int]] = None, + alignment: Optional[str] = None, + vertical_alignment: Optional[str] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """Format a specific table cell.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + if shape_index < 0 or shape_index >= len(slide.shapes): + return { + "error": f"Invalid shape index: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}" + } + + shape = slide.shapes[shape_index] + + try: + if not hasattr(shape, 'table'): + return { + "error": f"Shape at index {shape_index} is not a table" + } + + table = shape.table + + if row < 0 or row >= len(table.rows): + return { + "error": f"Invalid row index: {row}. Available rows: 0-{len(table.rows) - 1}" + } + + if col < 0 or col >= len(table.columns): + return { + "error": f"Invalid column index: {col}. Available columns: 0-{len(table.columns) - 1}" + } + + cell = table.cell(row, col) + + ppt_utils.format_table_cell( + cell, + font_size=font_size, + font_name=font_name, + bold=bold, + italic=italic, + color=tuple(color) if color else None, + bg_color=tuple(bg_color) if bg_color else None, + alignment=alignment, + vertical_alignment=vertical_alignment + ) + + return { + "message": f"Formatted cell at row {row}, column {col} in table at shape index {shape_index} on slide {slide_index}" + } + except Exception as e: + return { + "error": f"Failed to format table cell: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Add Shape", + ), + ) + def add_shape( + slide_index: int, + shape_type: str, + left: float, + top: float, + width: float, + height: float, + fill_color: Optional[List[int]] = None, + line_color: Optional[List[int]] = None, + line_width: Optional[float] = None, + text: Optional[str] = None, # Add text to shape + font_size: Optional[int] = None, + font_color: Optional[List[int]] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """Add an auto shape to a slide with enhanced options.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + # Use the direct implementation that bypasses the enum issues + shape = add_shape_direct(slide, shape_type, left, top, width, height) + + # Format the shape if formatting options are provided + if any([fill_color, line_color, line_width]): + ppt_utils.format_shape( + shape, + fill_color=tuple(fill_color) if fill_color else None, + line_color=tuple(line_color) if line_color else None, + line_width=line_width + ) + + # Add text to shape if provided + if text and hasattr(shape, 'text_frame'): + shape.text_frame.text = text + if font_size or font_color: + ppt_utils.format_text( + shape.text_frame, + font_size=font_size, + color=tuple(font_color) if font_color else None + ) + + return { + "message": f"Added {shape_type} shape to slide {slide_index}", + "shape_index": len(slide.shapes) - 1 + } + except ValueError as e: + return { + "error": str(e) + } + except Exception as e: + return { + "error": f"Failed to add shape '{shape_type}': {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Add Chart", + ), + ) + def add_chart( + slide_index: int, + chart_type: str, + left: float, + top: float, + width: float, + height: float, + categories: List[str], + series_names: List[str], + series_values: List[List[float]], + has_legend: bool = True, + legend_position: str = "right", + has_data_labels: bool = False, + title: Optional[str] = None, + x_axis_title: Optional[str] = None, + y_axis_title: Optional[str] = None, + color_scheme: Optional[str] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """Add a chart to a slide with comprehensive formatting options.""" + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + # Validate chart type + valid_chart_types = [ + 'column', 'stacked_column', 'bar', 'stacked_bar', 'line', + 'line_markers', 'pie', 'doughnut', 'area', 'stacked_area', + 'scatter', 'radar', 'radar_markers' + ] + if chart_type.lower() not in valid_chart_types: + return { + "error": f"Invalid chart type: '{chart_type}'. Valid types are: {', '.join(valid_chart_types)}" + } + + # Validate series data + if len(series_names) != len(series_values): + return { + "error": f"Number of series names ({len(series_names)}) must match number of series values ({len(series_values)})" + } + + if not categories: + return { + "error": "Categories list cannot be empty" + } + + # Validate that all series have the same number of values as categories + for i, values in enumerate(series_values): + if len(values) != len(categories): + return { + "error": f"Series '{series_names[i]}' has {len(values)} values but there are {len(categories)} categories" + } + + try: + # Add the chart + chart = ppt_utils.add_chart( + slide, chart_type, left, top, width, height, + categories, series_names, series_values + ) + + if chart is None: + return {"error": "Failed to create chart"} + + # Format the chart + ppt_utils.format_chart( + chart, + has_legend=has_legend, + legend_position=legend_position, + has_data_labels=has_data_labels, + title=title, + x_axis_title=x_axis_title, + y_axis_title=y_axis_title, + color_scheme=color_scheme + ) + + return { + "message": f"Added {chart_type} chart to slide {slide_index}", + "shape_index": len(slide.shapes) - 1, + "chart_type": chart_type, + "series_count": len(series_names), + "categories_count": len(categories) + } + except Exception as e: + return { + "error": f"Failed to add chart: {str(e)}" + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/template_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/template_tools.py new file mode 100644 index 00000000..e6f81190 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/template_tools.py @@ -0,0 +1,552 @@ +""" +Enhanced template-based slide creation tools for PowerPoint MCP Server. +Handles template application, template management, automated slide generation, +and advanced features like dynamic sizing, auto-wrapping, and visual effects. +""" +from typing import Dict, List, Optional, Any +from mcp.server.fastmcp import FastMCP +from mcp.types import ToolAnnotations +import utils.template_utils as template_utils + + +def register_template_tools(app: FastMCP, presentations: Dict, get_current_presentation_id): + """Register template-based tools with the FastMCP app""" + + @app.tool( + annotations=ToolAnnotations( + title="List Slide Templates", + readOnlyHint=True, + ), + ) + def list_slide_templates() -> Dict: + """List all available slide layout templates.""" + try: + available_templates = template_utils.get_available_templates() + usage_examples = template_utils.get_template_usage_examples() + + return { + "available_templates": available_templates, + "total_templates": len(available_templates), + "usage_examples": usage_examples, + "message": "Use apply_slide_template to apply templates to slides" + } + except Exception as e: + return { + "error": f"Failed to list templates: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Apply Slide Template", + ), + ) + def apply_slide_template( + slide_index: int, + template_id: str, + color_scheme: str = "modern_blue", + content_mapping: Optional[Dict[str, str]] = None, + image_paths: Optional[Dict[str, str]] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """ + Apply a structured layout template to an existing slide. + This modifies slide layout and content structure using predefined templates. + + Args: + slide_index: Index of the slide to apply template to + template_id: ID of the template to apply (e.g., 'title_slide', 'text_with_image') + color_scheme: Color scheme to use ('modern_blue', 'corporate_gray', 'elegant_green', 'warm_red') + content_mapping: Dictionary mapping element roles to custom content + image_paths: Dictionary mapping image element roles to file paths + presentation_id: Presentation ID (uses current if None) + """ + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + result = template_utils.apply_slide_template( + slide, template_id, color_scheme, + content_mapping or {}, image_paths or {} + ) + + if result['success']: + return { + "message": f"Applied template '{template_id}' to slide {slide_index}", + "slide_index": slide_index, + "template_applied": result + } + else: + return { + "error": f"Failed to apply template: {result.get('error', 'Unknown error')}" + } + + except Exception as e: + return { + "error": f"Failed to apply template: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Create Slide from Template", + ), + ) + def create_slide_from_template( + template_id: str, + color_scheme: str = "modern_blue", + content_mapping: Optional[Dict[str, str]] = None, + image_paths: Optional[Dict[str, str]] = None, + layout_index: int = 1, + presentation_id: Optional[str] = None + ) -> Dict: + """ + Create a new slide using a layout template. + + Args: + template_id: ID of the template to use (e.g., 'title_slide', 'text_with_image') + color_scheme: Color scheme to use ('modern_blue', 'corporate_gray', 'elegant_green', 'warm_red') + content_mapping: Dictionary mapping element roles to custom content + image_paths: Dictionary mapping image element roles to file paths + layout_index: PowerPoint layout index to use as base (default: 1) + presentation_id: Presentation ID (uses current if None) + """ + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + # Validate layout index + if layout_index < 0 or layout_index >= len(pres.slide_layouts): + return { + "error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}" + } + + try: + # Add new slide + layout = pres.slide_layouts[layout_index] + slide = pres.slides.add_slide(layout) + slide_index = len(pres.slides) - 1 + + # Apply template + result = template_utils.apply_slide_template( + slide, template_id, color_scheme, + content_mapping or {}, image_paths or {} + ) + + if result['success']: + return { + "message": f"Created slide {slide_index} using template '{template_id}'", + "slide_index": slide_index, + "template_applied": result + } + else: + return { + "error": f"Failed to apply template to new slide: {result.get('error', 'Unknown error')}" + } + + except Exception as e: + return { + "error": f"Failed to create slide from template: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Create Presentation from Templates", + ), + ) + def create_presentation_from_templates( + template_sequence: List[Dict[str, Any]], + color_scheme: str = "modern_blue", + presentation_title: Optional[str] = None, + presentation_id: Optional[str] = None + ) -> Dict: + """ + Create a complete presentation from a sequence of templates. + + Args: + template_sequence: List of template configurations, each containing: + - template_id: Template to use + - content: Content mapping for the template + - images: Image path mapping for the template + color_scheme: Color scheme to apply to all slides + presentation_title: Optional title for the presentation + presentation_id: Presentation ID (uses current if None) + + Example template_sequence: + [ + { + "template_id": "title_slide", + "content": { + "title": "My Presentation", + "subtitle": "Annual Report 2024", + "author": "John Doe" + } + }, + { + "template_id": "text_with_image", + "content": { + "title": "Key Results", + "content": "• Achievement 1\\n• Achievement 2" + }, + "images": { + "supporting": "/path/to/image.jpg" + } + } + ] + """ + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if not template_sequence: + return { + "error": "Template sequence cannot be empty" + } + + try: + # Set presentation title if provided + if presentation_title: + pres.core_properties.title = presentation_title + + # Create slides from template sequence + result = template_utils.create_presentation_from_template_sequence( + pres, template_sequence, color_scheme + ) + + if result['success']: + return { + "message": f"Created presentation with {result['total_slides']} slides", + "presentation_id": pres_id, + "creation_result": result, + "total_slides": len(pres.slides) + } + else: + return { + "warning": "Presentation created with some errors", + "presentation_id": pres_id, + "creation_result": result, + "total_slides": len(pres.slides) + } + + except Exception as e: + return { + "error": f"Failed to create presentation from templates: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Get Template Info", + readOnlyHint=True, + ), + ) + def get_template_info(template_id: str) -> Dict: + """ + Get detailed information about a specific template. + + Args: + template_id: ID of the template to get information about + """ + try: + templates_data = template_utils.load_slide_templates() + + if template_id not in templates_data.get('templates', {}): + available_templates = list(templates_data.get('templates', {}).keys()) + return { + "error": f"Template '{template_id}' not found", + "available_templates": available_templates + } + + template = templates_data['templates'][template_id] + + # Extract element information + elements_info = [] + for element in template.get('elements', []): + element_info = { + "type": element.get('type'), + "role": element.get('role'), + "position": element.get('position'), + "placeholder_text": element.get('placeholder_text', ''), + "styling_options": list(element.get('styling', {}).keys()) + } + elements_info.append(element_info) + + return { + "template_id": template_id, + "name": template.get('name'), + "description": template.get('description'), + "layout_type": template.get('layout_type'), + "elements": elements_info, + "element_count": len(elements_info), + "has_background": 'background' in template, + "background_type": template.get('background', {}).get('type'), + "color_schemes": list(templates_data.get('color_schemes', {}).keys()), + "usage_tip": f"Use create_slide_from_template with template_id='{template_id}' to create a slide with this layout" + } + + except Exception as e: + return { + "error": f"Failed to get template info: {str(e)}" + } + + @app.tool( + annotations=ToolAnnotations( + title="Auto Generate Presentation", + ), + ) + def auto_generate_presentation( + topic: str, + slide_count: int = 5, + presentation_type: str = "business", + color_scheme: str = "modern_blue", + include_charts: bool = True, + include_images: bool = False, + presentation_id: Optional[str] = None + ) -> Dict: + """ + Automatically generate a presentation based on topic and preferences. + + Args: + topic: Main topic/theme for the presentation + slide_count: Number of slides to generate (3-20) + presentation_type: Type of presentation ('business', 'academic', 'creative') + color_scheme: Color scheme to use + include_charts: Whether to include chart slides + include_images: Whether to include image placeholders + presentation_id: Presentation ID (uses current if None) + """ + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + if slide_count < 3 or slide_count > 20: + return { + "error": "Slide count must be between 3 and 20" + } + + try: + # Define presentation structures based on type + if presentation_type == "business": + base_templates = [ + ("title_slide", {"title": f"{topic}", "subtitle": "Executive Presentation", "author": "Business Team"}), + ("agenda_slide", {"agenda_items": "1. Executive Summary\n\n2. Current Situation\n\n3. Analysis & Insights\n\n4. Recommendations\n\n5. Next Steps"}), + ("key_metrics_dashboard", {"title": "Key Performance Indicators"}), + ("text_with_image", {"title": "Current Situation", "content": f"Overview of {topic}:\n• Current status\n• Key challenges\n• Market position"}), + ("two_column_text", {"title": "Analysis", "content_left": "Strengths:\n• Advantage 1\n• Advantage 2\n• Advantage 3", "content_right": "Opportunities:\n• Opportunity 1\n• Opportunity 2\n• Opportunity 3"}), + ] + if include_charts: + base_templates.append(("chart_comparison", {"title": "Performance Comparison"})) + base_templates.append(("thank_you_slide", {"contact": "Thank you for your attention\nQuestions & Discussion"})) + + elif presentation_type == "academic": + base_templates = [ + ("title_slide", {"title": f"Research on {topic}", "subtitle": "Academic Study", "author": "Research Team"}), + ("agenda_slide", {"agenda_items": "1. Introduction\n\n2. Literature Review\n\n3. Methodology\n\n4. Results\n\n5. Conclusions"}), + ("text_with_image", {"title": "Introduction", "content": f"Research focus on {topic}:\n• Background\n• Problem statement\n• Research questions"}), + ("two_column_text", {"title": "Methodology", "content_left": "Approach:\n• Method 1\n• Method 2\n• Method 3", "content_right": "Data Sources:\n• Source 1\n• Source 2\n• Source 3"}), + ("data_table_slide", {"title": "Results Summary"}), + ] + if include_charts: + base_templates.append(("chart_comparison", {"title": "Data Analysis"})) + base_templates.append(("thank_you_slide", {"contact": "Questions & Discussion\nContact: research@university.edu"})) + + else: # creative + base_templates = [ + ("title_slide", {"title": f"Creative Vision: {topic}", "subtitle": "Innovative Concepts", "author": "Creative Team"}), + ("full_image_slide", {"overlay_title": f"Exploring {topic}", "overlay_subtitle": "Creative possibilities"}), + ("three_column_layout", {"title": "Creative Concepts"}), + ("quote_testimonial", {"quote_text": f"Innovation in {topic} requires thinking beyond conventional boundaries", "attribution": "— Creative Director"}), + ("process_flow", {"title": "Creative Process"}), + ] + if include_charts: + base_templates.append(("key_metrics_dashboard", {"title": "Impact Metrics"})) + base_templates.append(("thank_you_slide", {"contact": "Let's create something amazing together\ncreative@studio.com"})) + + # Adjust templates to match requested slide count + template_sequence = [] + templates_to_use = base_templates[:slide_count] + + # If we need more slides, add content slides + while len(templates_to_use) < slide_count: + if include_images: + templates_to_use.insert(-1, ("text_with_image", {"title": f"{topic} - Additional Topic", "content": "• Key point\n• Supporting detail\n• Additional insight"})) + else: + templates_to_use.insert(-1, ("two_column_text", {"title": f"{topic} - Analysis", "content_left": "Key Points:\n• Point 1\n• Point 2", "content_right": "Details:\n• Detail 1\n• Detail 2"})) + + # Convert to proper template sequence format + for i, (template_id, content) in enumerate(templates_to_use): + template_config = { + "template_id": template_id, + "content": content + } + template_sequence.append(template_config) + + # Create the presentation + result = template_utils.create_presentation_from_template_sequence( + presentations[pres_id], template_sequence, color_scheme + ) + + return { + "message": f"Auto-generated {slide_count}-slide presentation on '{topic}'", + "topic": topic, + "presentation_type": presentation_type, + "color_scheme": color_scheme, + "slide_count": slide_count, + "generation_result": result, + "templates_used": [t[0] for t in templates_to_use] + } + + except Exception as e: + return { + "error": f"Failed to auto-generate presentation: {str(e)}" + } + + # Text optimization tools + + + @app.tool( + annotations=ToolAnnotations( + title="Optimize Slide Text", + ), + ) + def optimize_slide_text( + slide_index: int, + auto_resize: bool = True, + auto_wrap: bool = True, + optimize_spacing: bool = True, + min_font_size: int = 8, + max_font_size: int = 36, + presentation_id: Optional[str] = None + ) -> Dict: + """ + Optimize text elements on a slide for better readability and fit. + + Args: + slide_index: Index of the slide to optimize + auto_resize: Whether to automatically resize fonts to fit containers + auto_wrap: Whether to apply intelligent text wrapping + optimize_spacing: Whether to optimize line spacing + min_font_size: Minimum allowed font size + max_font_size: Maximum allowed font size + presentation_id: Presentation ID (uses current if None) + """ + pres_id = presentation_id if presentation_id is not None else get_current_presentation_id() + + if pres_id is None or pres_id not in presentations: + return { + "error": "No presentation is currently loaded or the specified ID is invalid" + } + + pres = presentations[pres_id] + + if slide_index < 0 or slide_index >= len(pres.slides): + return { + "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}" + } + + slide = pres.slides[slide_index] + + try: + optimizations_applied = [] + manager = template_utils.get_enhanced_template_manager() + + # Analyze each text shape on the slide + for i, shape in enumerate(slide.shapes): + if hasattr(shape, 'text_frame') and shape.text_frame.text: + text = shape.text_frame.text + + # Calculate container dimensions + container_width = shape.width.inches + container_height = shape.height.inches + + shape_optimizations = [] + + # Apply auto-resize if enabled + if auto_resize: + optimal_size = template_utils.calculate_dynamic_font_size( + text, container_width, container_height + ) + optimal_size = max(min_font_size, min(max_font_size, optimal_size)) + + # Apply the calculated font size + for paragraph in shape.text_frame.paragraphs: + for run in paragraph.runs: + run.font.size = template_utils.Pt(optimal_size) + + shape_optimizations.append(f"Font resized to {optimal_size}pt") + + # Apply auto-wrap if enabled + if auto_wrap: + current_font_size = 14 # Default assumption + if shape.text_frame.paragraphs and shape.text_frame.paragraphs[0].runs: + if shape.text_frame.paragraphs[0].runs[0].font.size: + current_font_size = shape.text_frame.paragraphs[0].runs[0].font.size.pt + + wrapped_text = template_utils.wrap_text_automatically( + text, container_width, current_font_size + ) + + if wrapped_text != text: + shape.text_frame.text = wrapped_text + shape_optimizations.append("Text wrapped automatically") + + # Optimize spacing if enabled + if optimize_spacing: + text_length = len(text) + if text_length > 300: + line_spacing = 1.4 + elif text_length > 150: + line_spacing = 1.3 + else: + line_spacing = 1.2 + + for paragraph in shape.text_frame.paragraphs: + paragraph.line_spacing = line_spacing + + shape_optimizations.append(f"Line spacing set to {line_spacing}") + + if shape_optimizations: + optimizations_applied.append({ + "shape_index": i, + "optimizations": shape_optimizations + }) + + return { + "message": f"Optimized {len(optimizations_applied)} text elements on slide {slide_index}", + "slide_index": slide_index, + "optimizations_applied": optimizations_applied, + "settings": { + "auto_resize": auto_resize, + "auto_wrap": auto_wrap, + "optimize_spacing": optimize_spacing, + "font_size_range": f"{min_font_size}-{max_font_size}pt" + } + } + + except Exception as e: + return { + "error": f"Failed to optimize slide text: {str(e)}" + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/transition_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/transition_tools.py new file mode 100644 index 00000000..16dcc8f0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/transition_tools.py @@ -0,0 +1,80 @@ +""" +Slide transition management tools for PowerPoint MCP Server. +Implements slide transition and timing capabilities. +""" + +from typing import Dict, List, Optional, Any +from mcp.types import ToolAnnotations + +def register_transition_tools(app, presentations, get_current_presentation_id, validate_parameters, + is_positive, is_non_negative, is_in_range, is_valid_rgb): + """Register slide transition management tools with the FastMCP app.""" + + @app.tool( + annotations=ToolAnnotations( + title="Manage Slide Transitions", + ), + ) + def manage_slide_transitions( + slide_index: int, + operation: str, + transition_type: str = None, + duration: float = 1.0, + presentation_id: str = None + ) -> Dict: + """ + Manage slide transitions and timing. + + Args: + slide_index: Index of the slide (0-based) + operation: Operation type ("set", "remove", "get") + transition_type: Type of transition (basic support) + duration: Duration of transition in seconds + presentation_id: Optional presentation ID (uses current if not provided) + + Returns: + Dictionary with transition information + """ + try: + # Get presentation + pres_id = presentation_id or get_current_presentation_id() + if pres_id not in presentations: + return {"error": "Presentation not found"} + + pres = presentations[pres_id] + + # Validate slide index + if not (0 <= slide_index < len(pres.slides)): + return {"error": f"Slide index {slide_index} out of range"} + + slide = pres.slides[slide_index] + + if operation == "get": + # Get current transition info (limited python-pptx support) + return { + "message": f"Transition info for slide {slide_index}", + "slide_index": slide_index, + "note": "Transition reading has limited support in python-pptx" + } + + elif operation == "set": + return { + "message": f"Transition setting requested for slide {slide_index}", + "slide_index": slide_index, + "transition_type": transition_type, + "duration": duration, + "note": "Transition setting has limited support in python-pptx - this is a placeholder for future enhancement" + } + + elif operation == "remove": + return { + "message": f"Transition removal requested for slide {slide_index}", + "slide_index": slide_index, + "note": "Transition removal has limited support in python-pptx - this is a placeholder for future enhancement" + } + + else: + return {"error": f"Unsupported operation: {operation}. Use 'set', 'remove', or 'get'"} + + except Exception as e: + return {"error": f"Failed to manage slide transitions: {str(e)}"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/__init__.py new file mode 100644 index 00000000..43d45035 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/__init__.py @@ -0,0 +1,69 @@ +""" +PowerPoint utilities package. +Organized utility functions for PowerPoint manipulation. +""" + +from .core_utils import * +from .presentation_utils import * +from .content_utils import * +from .design_utils import * +from .validation_utils import * + +__all__ = [ + # Core utilities + "safe_operation", + "try_multiple_approaches", + + # Presentation utilities + "create_presentation", + "open_presentation", + "save_presentation", + "create_presentation_from_template", + "get_presentation_info", + "get_template_info", + "set_core_properties", + "get_core_properties", + + # Content utilities + "add_slide", + "get_slide_info", + "set_title", + "populate_placeholder", + "add_bullet_points", + "add_textbox", + "format_text", + "format_text_advanced", + "add_image", + "add_table", + "format_table_cell", + "add_chart", + "format_chart", + + # Design utilities + "get_professional_color", + "get_professional_font", + "get_color_schemes", + "add_professional_slide", + "apply_professional_theme", + "enhance_existing_slide", + "apply_professional_image_enhancement", + "enhance_image_with_pillow", + "set_slide_gradient_background", + "create_professional_gradient_background", + "format_shape", + "apply_picture_shadow", + "apply_picture_reflection", + "apply_picture_glow", + "apply_picture_soft_edges", + "apply_picture_rotation", + "apply_picture_transparency", + "apply_picture_bevel", + "apply_picture_filter", + "analyze_font_file", + "optimize_font_for_presentation", + "get_font_recommendations", + + # Validation utilities + "validate_text_fit", + "validate_and_fix_slide" +] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/content_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/content_utils.py new file mode 100644 index 00000000..27266d10 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/content_utils.py @@ -0,0 +1,579 @@ +""" +Content management utilities for PowerPoint MCP Server. +Functions for slides, text, images, tables, charts, and shapes. +""" +from pptx import Presentation +from pptx.chart.data import CategoryChartData +from pptx.enum.chart import XL_CHART_TYPE +from pptx.enum.text import PP_ALIGN +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from typing import Dict, List, Tuple, Optional, Any +import tempfile +import os +import base64 + + +def add_slide(presentation: Presentation, layout_index: int = 1) -> Tuple: + """ + Add a slide to the presentation. + + Args: + presentation: The Presentation object + layout_index: Index of the slide layout to use + + Returns: + A tuple containing the slide and its layout + """ + layout = presentation.slide_layouts[layout_index] + slide = presentation.slides.add_slide(layout) + return slide, layout + + +def get_slide_info(slide, slide_index: int) -> Dict: + """ + Get information about a specific slide. + + Args: + slide: The slide object + slide_index: Index of the slide + + Returns: + Dictionary containing slide information + """ + try: + placeholders = [] + for placeholder in slide.placeholders: + placeholder_info = { + "idx": placeholder.placeholder_format.idx, + "type": str(placeholder.placeholder_format.type), + "name": placeholder.name + } + placeholders.append(placeholder_info) + + shapes = [] + for i, shape in enumerate(slide.shapes): + shape_info = { + "index": i, + "name": shape.name, + "shape_type": str(shape.shape_type), + "left": shape.left, + "top": shape.top, + "width": shape.width, + "height": shape.height + } + shapes.append(shape_info) + + return { + "slide_index": slide_index, + "layout_name": slide.slide_layout.name, + "placeholder_count": len(placeholders), + "placeholders": placeholders, + "shape_count": len(shapes), + "shapes": shapes + } + except Exception as e: + raise Exception(f"Failed to get slide info: {str(e)}") + + +def set_title(slide, title: str) -> None: + """ + Set the title of a slide. + + Args: + slide: The slide object + title: The title text + """ + if slide.shapes.title: + slide.shapes.title.text = title + + +def populate_placeholder(slide, placeholder_idx: int, text: str) -> None: + """ + Populate a placeholder with text. + + Args: + slide: The slide object + placeholder_idx: The index of the placeholder + text: The text to add + """ + placeholder = slide.placeholders[placeholder_idx] + placeholder.text = text + + +def add_bullet_points(placeholder, bullet_points: List[str]) -> None: + """ + Add bullet points to a placeholder. + + Args: + placeholder: The placeholder object + bullet_points: List of bullet point texts + """ + text_frame = placeholder.text_frame + text_frame.clear() + + for i, point in enumerate(bullet_points): + p = text_frame.add_paragraph() + p.text = point + p.level = 0 + + +def add_textbox(slide, left: float, top: float, width: float, height: float, text: str, + font_size: int = None, font_name: str = None, bold: bool = None, + italic: bool = None, underline: bool = None, + color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None, + alignment: str = None, vertical_alignment: str = None, + auto_fit: bool = True) -> Any: + """ + Add a textbox to a slide with formatting options. + + Args: + slide: The slide object + left: Left position in inches + top: Top position in inches + width: Width in inches + height: Height in inches + text: Text content + font_size: Font size in points + font_name: Font name + bold: Whether text should be bold + italic: Whether text should be italic + underline: Whether text should be underlined + color: RGB color tuple (r, g, b) + bg_color: Background RGB color tuple (r, g, b) + alignment: Text alignment ('left', 'center', 'right', 'justify') + vertical_alignment: Vertical alignment ('top', 'middle', 'bottom') + auto_fit: Whether to auto-fit text + + Returns: + The created textbox shape + """ + textbox = slide.shapes.add_textbox( + Inches(left), Inches(top), Inches(width), Inches(height) + ) + + textbox.text_frame.text = text + + # Apply formatting if provided + if any([font_size, font_name, bold, italic, underline, color, bg_color, alignment, vertical_alignment]): + format_text_advanced( + textbox.text_frame, + font_size=font_size, + font_name=font_name, + bold=bold, + italic=italic, + underline=underline, + color=color, + bg_color=bg_color, + alignment=alignment, + vertical_alignment=vertical_alignment + ) + + return textbox + + +def format_text(text_frame, font_size: int = None, font_name: str = None, + bold: bool = None, italic: bool = None, color: Tuple[int, int, int] = None, + alignment: str = None) -> None: + """ + Format text in a text frame. + + Args: + text_frame: The text frame to format + font_size: Font size in points + font_name: Font name + bold: Whether text should be bold + italic: Whether text should be italic + color: RGB color tuple (r, g, b) + alignment: Text alignment ('left', 'center', 'right', 'justify') + """ + alignment_map = { + 'left': PP_ALIGN.LEFT, + 'center': PP_ALIGN.CENTER, + 'right': PP_ALIGN.RIGHT, + 'justify': PP_ALIGN.JUSTIFY + } + + for paragraph in text_frame.paragraphs: + if alignment and alignment in alignment_map: + paragraph.alignment = alignment_map[alignment] + + for run in paragraph.runs: + font = run.font + + if font_size is not None: + font.size = Pt(font_size) + if font_name is not None: + font.name = font_name + if bold is not None: + font.bold = bold + if italic is not None: + font.italic = italic + if color is not None: + r, g, b = color + font.color.rgb = RGBColor(r, g, b) + + +def format_text_advanced(text_frame, font_size: int = None, font_name: str = None, + bold: bool = None, italic: bool = None, underline: bool = None, + color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None, + alignment: str = None, vertical_alignment: str = None) -> Dict: + """ + Advanced text formatting with comprehensive options. + + Args: + text_frame: The text frame to format + font_size: Font size in points + font_name: Font name + bold: Whether text should be bold + italic: Whether text should be italic + underline: Whether text should be underlined + color: RGB color tuple (r, g, b) + bg_color: Background RGB color tuple (r, g, b) + alignment: Text alignment ('left', 'center', 'right', 'justify') + vertical_alignment: Vertical alignment ('top', 'middle', 'bottom') + + Returns: + Dictionary with formatting results + """ + result = { + 'success': True, + 'warnings': [] + } + + try: + alignment_map = { + 'left': PP_ALIGN.LEFT, + 'center': PP_ALIGN.CENTER, + 'right': PP_ALIGN.RIGHT, + 'justify': PP_ALIGN.JUSTIFY + } + + # Enable text wrapping + text_frame.word_wrap = True + + # Apply formatting to all paragraphs and runs + for paragraph in text_frame.paragraphs: + if alignment and alignment in alignment_map: + paragraph.alignment = alignment_map[alignment] + + for run in paragraph.runs: + font = run.font + + if font_size is not None: + font.size = Pt(font_size) + if font_name is not None: + font.name = font_name + if bold is not None: + font.bold = bold + if italic is not None: + font.italic = italic + if underline is not None: + font.underline = underline + if color is not None: + r, g, b = color + font.color.rgb = RGBColor(r, g, b) + + return result + + except Exception as e: + result['success'] = False + result['error'] = str(e) + return result + + +def add_image(slide, image_path: str, left: float, top: float, width: float = None, height: float = None) -> Any: + """ + Add an image to a slide. + + Args: + slide: The slide object + image_path: Path to the image file + left: Left position in inches + top: Top position in inches + width: Width in inches (optional) + height: Height in inches (optional) + + Returns: + The created image shape + """ + if width is not None and height is not None: + return slide.shapes.add_picture( + image_path, Inches(left), Inches(top), Inches(width), Inches(height) + ) + elif width is not None: + return slide.shapes.add_picture( + image_path, Inches(left), Inches(top), Inches(width) + ) + elif height is not None: + return slide.shapes.add_picture( + image_path, Inches(left), Inches(top), height=Inches(height) + ) + else: + return slide.shapes.add_picture( + image_path, Inches(left), Inches(top) + ) + + +def add_table(slide, rows: int, cols: int, left: float, top: float, width: float, height: float) -> Any: + """ + Add a table to a slide. + + Args: + slide: The slide object + rows: Number of rows + cols: Number of columns + left: Left position in inches + top: Top position in inches + width: Width in inches + height: Height in inches + + Returns: + The created table shape + """ + return slide.shapes.add_table( + rows, cols, Inches(left), Inches(top), Inches(width), Inches(height) + ) + + +def format_table_cell(cell, font_size: int = None, font_name: str = None, + bold: bool = None, italic: bool = None, + color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None, + alignment: str = None, vertical_alignment: str = None) -> None: + """ + Format a table cell. + + Args: + cell: The table cell object + font_size: Font size in points + font_name: Font name + bold: Whether text should be bold + italic: Whether text should be italic + color: RGB color tuple (r, g, b) + bg_color: Background RGB color tuple (r, g, b) + alignment: Text alignment + vertical_alignment: Vertical alignment + """ + # Format text + if any([font_size, font_name, bold, italic, color, alignment]): + format_text_advanced( + cell.text_frame, + font_size=font_size, + font_name=font_name, + bold=bold, + italic=italic, + color=color, + alignment=alignment + ) + + # Set background color + if bg_color: + cell.fill.solid() + cell.fill.fore_color.rgb = RGBColor(*bg_color) + + +def add_chart(slide, chart_type: str, left: float, top: float, width: float, height: float, + categories: List[str], series_names: List[str], series_values: List[List[float]]) -> Any: + """ + Add a chart to a slide. + + Args: + slide: The slide object + chart_type: Type of chart ('column', 'bar', 'line', 'pie', etc.) + left: Left position in inches + top: Top position in inches + width: Width in inches + height: Height in inches + categories: List of category names + series_names: List of series names + series_values: List of value lists for each series + + Returns: + The created chart object + """ + # Map chart type names to enum values + chart_type_map = { + 'column': XL_CHART_TYPE.COLUMN_CLUSTERED, + 'stacked_column': XL_CHART_TYPE.COLUMN_STACKED, + 'bar': XL_CHART_TYPE.BAR_CLUSTERED, + 'stacked_bar': XL_CHART_TYPE.BAR_STACKED, + 'line': XL_CHART_TYPE.LINE, + 'line_markers': XL_CHART_TYPE.LINE_MARKERS, + 'pie': XL_CHART_TYPE.PIE, + 'doughnut': XL_CHART_TYPE.DOUGHNUT, + 'area': XL_CHART_TYPE.AREA, + 'stacked_area': XL_CHART_TYPE.AREA_STACKED, + 'scatter': XL_CHART_TYPE.XY_SCATTER, + 'radar': XL_CHART_TYPE.RADAR, + 'radar_markers': XL_CHART_TYPE.RADAR_MARKERS + } + + xl_chart_type = chart_type_map.get(chart_type.lower(), XL_CHART_TYPE.COLUMN_CLUSTERED) + + # Create chart data + chart_data = CategoryChartData() + chart_data.categories = categories + + for i, series_name in enumerate(series_names): + if i < len(series_values): + chart_data.add_series(series_name, series_values[i]) + + # Add chart to slide + chart_shape = slide.shapes.add_chart( + xl_chart_type, Inches(left), Inches(top), Inches(width), Inches(height), chart_data + ) + + return chart_shape.chart + + +def format_chart(chart, has_legend: bool = True, legend_position: str = 'right', + has_data_labels: bool = False, title: str = None, + x_axis_title: str = None, y_axis_title: str = None, + color_scheme: str = None) -> None: + """ + Format a chart with various options. + + Args: + chart: The chart object + has_legend: Whether to show legend + legend_position: Position of legend ('right', 'top', 'bottom', 'left') + has_data_labels: Whether to show data labels + title: Chart title + x_axis_title: X-axis title + y_axis_title: Y-axis title + color_scheme: Color scheme to apply + """ + try: + # Set chart title + if title: + chart.chart_title.text_frame.text = title + + # Configure legend + if has_legend: + chart.has_legend = True + # Note: Legend position setting may vary by chart type + else: + chart.has_legend = False + + # Configure data labels + if has_data_labels: + for series in chart.series: + series.has_data_labels = True + + # Set axis titles if available + try: + if x_axis_title and hasattr(chart, 'category_axis'): + chart.category_axis.axis_title.text_frame.text = x_axis_title + if y_axis_title and hasattr(chart, 'value_axis'): + chart.value_axis.axis_title.text_frame.text = y_axis_title + except: + pass # Axis titles may not be available for all chart types + + except Exception: + pass # Graceful degradation for chart formatting + + +def extract_slide_text_content(slide) -> Dict: + """ + Extract all text content from a slide including placeholders and text shapes. + + Args: + slide: The slide object to extract text from + + Returns: + Dictionary containing all text content organized by source type + """ + try: + text_content = { + "slide_title": "", + "placeholders": [], + "text_shapes": [], + "table_text": [], + "all_text_combined": "" + } + + all_texts = [] + + # Extract title from slide if available + if hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title: + try: + title_text = slide.shapes.title.text_frame.text.strip() + if title_text: + text_content["slide_title"] = title_text + all_texts.append(title_text) + except: + pass + + # Extract text from all shapes + for i, shape in enumerate(slide.shapes): + shape_text_info = { + "shape_index": i, + "shape_name": shape.name, + "shape_type": str(shape.shape_type), + "text": "" + } + + try: + # Check if shape has text frame + if hasattr(shape, 'text_frame') and shape.text_frame: + text = shape.text_frame.text.strip() + if text: + shape_text_info["text"] = text + all_texts.append(text) + + # Categorize by shape type + if hasattr(shape, 'placeholder_format'): + # This is a placeholder + placeholder_info = shape_text_info.copy() + placeholder_info["placeholder_type"] = str(shape.placeholder_format.type) + placeholder_info["placeholder_idx"] = shape.placeholder_format.idx + text_content["placeholders"].append(placeholder_info) + else: + # This is a regular text shape + text_content["text_shapes"].append(shape_text_info) + + # Extract text from tables + elif hasattr(shape, 'table'): + table_texts = [] + table = shape.table + for row_idx, row in enumerate(table.rows): + row_texts = [] + for col_idx, cell in enumerate(row.cells): + cell_text = cell.text_frame.text.strip() + if cell_text: + row_texts.append(cell_text) + all_texts.append(cell_text) + if row_texts: + table_texts.append({ + "row": row_idx, + "cells": row_texts + }) + + if table_texts: + text_content["table_text"].append({ + "shape_index": i, + "shape_name": shape.name, + "table_content": table_texts + }) + + except Exception as e: + # Skip shapes that can't be processed + continue + + # Combine all text + text_content["all_text_combined"] = "\n".join(all_texts) + + return { + "success": True, + "text_content": text_content, + "total_text_shapes": len(text_content["placeholders"]) + len(text_content["text_shapes"]), + "has_title": bool(text_content["slide_title"]), + "has_tables": len(text_content["table_text"]) > 0 + } + + except Exception as e: + return { + "success": False, + "error": f"Failed to extract text content: {str(e)}", + "text_content": None + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/core_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/core_utils.py new file mode 100644 index 00000000..c19c66cd --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/core_utils.py @@ -0,0 +1,55 @@ +""" +Core utility functions for PowerPoint MCP Server. +Basic operations and error handling. +""" +from typing import Any, Callable, List, Tuple, Optional + + +def try_multiple_approaches(operation_name: str, approaches: List[Tuple[Callable, str]]) -> Tuple[Any, Optional[str]]: + """ + Try multiple approaches to perform an operation, returning the first successful result. + + Args: + operation_name: Name of the operation for error reporting + approaches: List of (approach_func, description) tuples to try + + Returns: + Tuple of (result, None) if any approach succeeded, or (None, error_messages) if all failed + """ + error_messages = [] + + for approach_func, description in approaches: + try: + result = approach_func() + return result, None + except Exception as e: + error_messages.append(f"{description}: {str(e)}") + + return None, f"Failed to {operation_name} after trying multiple approaches: {'; '.join(error_messages)}" + + +def safe_operation(operation_name: str, operation_func: Callable, error_message: Optional[str] = None, *args, **kwargs) -> Tuple[Any, Optional[str]]: + """ + Execute an operation safely with standard error handling. + + Args: + operation_name: Name of the operation for error reporting + operation_func: Function to execute + error_message: Custom error message (optional) + *args, **kwargs: Arguments to pass to the operation function + + Returns: + A tuple (result, error) where error is None if operation was successful + """ + try: + result = operation_func(*args, **kwargs) + return result, None + except ValueError as e: + error_msg = error_message or f"Invalid input for {operation_name}: {str(e)}" + return None, error_msg + except TypeError as e: + error_msg = error_message or f"Type error in {operation_name}: {str(e)}" + return None, error_msg + except Exception as e: + error_msg = error_message or f"Failed to execute {operation_name}: {str(e)}" + return None, error_msg \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/design_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/design_utils.py new file mode 100644 index 00000000..b2b22fc7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/design_utils.py @@ -0,0 +1,689 @@ +""" +Design and professional styling utilities for PowerPoint MCP Server. +Functions for themes, colors, fonts, backgrounds, and visual effects. +""" +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from typing import Dict, List, Tuple, Optional, Any +from PIL import Image, ImageEnhance, ImageFilter, ImageDraw +import tempfile +import os +from fontTools.ttLib import TTFont +from fontTools.subset import Subsetter + +# Professional color schemes +PROFESSIONAL_COLOR_SCHEMES = { + 'modern_blue': { + 'primary': (0, 120, 215), # Microsoft Blue + 'secondary': (40, 40, 40), # Dark Gray + 'accent1': (0, 176, 240), # Light Blue + 'accent2': (255, 192, 0), # Orange + 'light': (247, 247, 247), # Light Gray + 'text': (68, 68, 68), # Text Gray + }, + 'corporate_gray': { + 'primary': (68, 68, 68), # Charcoal + 'secondary': (0, 120, 215), # Blue + 'accent1': (89, 89, 89), # Medium Gray + 'accent2': (217, 217, 217), # Light Gray + 'light': (242, 242, 242), # Very Light Gray + 'text': (51, 51, 51), # Dark Text + }, + 'elegant_green': { + 'primary': (70, 136, 71), # Forest Green + 'secondary': (255, 255, 255), # White + 'accent1': (146, 208, 80), # Light Green + 'accent2': (112, 173, 71), # Medium Green + 'light': (238, 236, 225), # Cream + 'text': (89, 89, 89), # Gray Text + }, + 'warm_red': { + 'primary': (192, 80, 77), # Deep Red + 'secondary': (68, 68, 68), # Dark Gray + 'accent1': (230, 126, 34), # Orange + 'accent2': (241, 196, 15), # Yellow + 'light': (253, 253, 253), # White + 'text': (44, 62, 80), # Blue Gray + } +} + +# Professional typography settings +PROFESSIONAL_FONTS = { + 'title': { + 'name': 'Segoe UI', + 'size_large': 36, + 'size_medium': 28, + 'size_small': 24, + 'bold': True + }, + 'subtitle': { + 'name': 'Segoe UI Light', + 'size_large': 20, + 'size_medium': 18, + 'size_small': 16, + 'bold': False + }, + 'body': { + 'name': 'Segoe UI', + 'size_large': 16, + 'size_medium': 14, + 'size_small': 12, + 'bold': False + }, + 'caption': { + 'name': 'Segoe UI', + 'size_large': 12, + 'size_medium': 10, + 'size_small': 9, + 'bold': False + } +} + + +def get_professional_color(scheme_name: str, color_type: str) -> Tuple[int, int, int]: + """ + Get a professional color from predefined color schemes. + + Args: + scheme_name: Name of the color scheme + color_type: Type of color ('primary', 'secondary', 'accent1', 'accent2', 'light', 'text') + + Returns: + RGB color tuple (r, g, b) + """ + if scheme_name not in PROFESSIONAL_COLOR_SCHEMES: + scheme_name = 'modern_blue' # Default fallback + + scheme = PROFESSIONAL_COLOR_SCHEMES[scheme_name] + return scheme.get(color_type, scheme['primary']) + + +def get_professional_font(font_type: str, size_category: str = 'medium') -> Dict: + """ + Get professional font settings. + + Args: + font_type: Type of font ('title', 'subtitle', 'body', 'caption') + size_category: Size category ('large', 'medium', 'small') + + Returns: + Dictionary with font settings + """ + if font_type not in PROFESSIONAL_FONTS: + font_type = 'body' # Default fallback + + font_config = PROFESSIONAL_FONTS[font_type] + size_key = f'size_{size_category}' + + return { + 'name': font_config['name'], + 'size': font_config.get(size_key, font_config['size_medium']), + 'bold': font_config['bold'] + } + + +def get_color_schemes() -> Dict: + """ + Get all available professional color schemes. + + Returns: + Dictionary of all color schemes with their color values + """ + return { + "available_schemes": list(PROFESSIONAL_COLOR_SCHEMES.keys()), + "schemes": PROFESSIONAL_COLOR_SCHEMES, + "color_types": ["primary", "secondary", "accent1", "accent2", "light", "text"], + "description": "Professional color schemes optimized for business presentations" + } + + +def add_professional_slide(presentation: Presentation, slide_type: str = 'title_content', + color_scheme: str = 'modern_blue', title: str = None, + content: List[str] = None) -> Dict: + """ + Add a professionally designed slide. + + Args: + presentation: The Presentation object + slide_type: Type of slide ('title', 'title_content', 'content', 'blank') + color_scheme: Color scheme to apply + title: Slide title + content: List of content items + + Returns: + Dictionary with slide creation results + """ + # Map slide types to layout indices + layout_map = { + 'title': 0, # Title slide + 'title_content': 1, # Title and content + 'content': 6, # Content only + 'blank': 6 # Blank layout + } + + layout_index = layout_map.get(slide_type, 1) + + try: + layout = presentation.slide_layouts[layout_index] + slide = presentation.slides.add_slide(layout) + + # Set title if provided + if title and slide.shapes.title: + slide.shapes.title.text = title + + # Add content if provided + if content and len(slide.placeholders) > 1: + content_placeholder = slide.placeholders[1] + content_text = '\n'.join([f"• {item}" for item in content]) + content_placeholder.text = content_text + + return { + "success": True, + "slide_index": len(presentation.slides) - 1, + "slide_type": slide_type, + "color_scheme": color_scheme + } + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def apply_professional_theme(presentation: Presentation, color_scheme: str = 'modern_blue', + apply_to_existing: bool = True) -> Dict: + """ + Apply a professional theme to the presentation. + + Args: + presentation: The Presentation object + color_scheme: Color scheme to apply + apply_to_existing: Whether to apply to existing slides + + Returns: + Dictionary with theme application results + """ + try: + # This is a placeholder implementation as theme application + # requires deep manipulation of presentation XML + return { + "success": True, + "color_scheme": color_scheme, + "slides_affected": len(presentation.slides) if apply_to_existing else 0, + "message": f"Applied {color_scheme} theme to presentation" + } + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def enhance_existing_slide(slide, color_scheme: str = 'modern_blue', + enhance_title: bool = True, enhance_content: bool = True, + enhance_shapes: bool = True, enhance_charts: bool = True) -> Dict: + """ + Enhance an existing slide with professional styling. + + Args: + slide: The slide object + color_scheme: Color scheme to apply + enhance_title: Whether to enhance title formatting + enhance_content: Whether to enhance content formatting + enhance_shapes: Whether to enhance shape formatting + enhance_charts: Whether to enhance chart formatting + + Returns: + Dictionary with enhancement results + """ + enhancements_applied = [] + + try: + # Enhance title + if enhance_title and slide.shapes.title: + primary_color = get_professional_color(color_scheme, 'primary') + title_font = get_professional_font('title', 'large') + # Apply title formatting (simplified) + enhancements_applied.append("title") + + # Enhance other shapes + if enhance_shapes: + for shape in slide.shapes: + if hasattr(shape, 'text_frame') and shape != slide.shapes.title: + # Apply content formatting (simplified) + pass + enhancements_applied.append("shapes") + + return { + "success": True, + "enhancements_applied": enhancements_applied, + "color_scheme": color_scheme + } + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def set_slide_gradient_background(slide, start_color: Tuple[int, int, int], + end_color: Tuple[int, int, int], direction: str = "horizontal") -> None: + """ + Set a gradient background for a slide using a generated image. + + Args: + slide: The slide object + start_color: Starting RGB color tuple + end_color: Ending RGB color tuple + direction: Gradient direction ('horizontal', 'vertical', 'diagonal') + """ + try: + # Create gradient image + width, height = 1920, 1080 # Standard slide dimensions + gradient_img = create_gradient_image(width, height, start_color, end_color, direction) + + # Save to temporary file + with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file: + gradient_img.save(temp_file.name, 'PNG') + temp_path = temp_file.name + + # Add as background image (simplified - actual implementation would need XML manipulation) + try: + slide.shapes.add_picture(temp_path, 0, 0, Inches(10), Inches(7.5)) + finally: + # Clean up temporary file + if os.path.exists(temp_path): + os.unlink(temp_path) + + except Exception: + pass # Graceful fallback + + +def create_professional_gradient_background(slide, color_scheme: str = 'modern_blue', + style: str = 'subtle', direction: str = 'diagonal') -> None: + """ + Create a professional gradient background using predefined color schemes. + + Args: + slide: The slide object + color_scheme: Professional color scheme to use + style: Gradient style ('subtle', 'bold', 'accent') + direction: Gradient direction ('horizontal', 'vertical', 'diagonal') + """ + # Get colors based on style + if style == 'subtle': + start_color = get_professional_color(color_scheme, 'light') + end_color = get_professional_color(color_scheme, 'secondary') + elif style == 'bold': + start_color = get_professional_color(color_scheme, 'primary') + end_color = get_professional_color(color_scheme, 'accent1') + else: # accent + start_color = get_professional_color(color_scheme, 'accent1') + end_color = get_professional_color(color_scheme, 'accent2') + + set_slide_gradient_background(slide, start_color, end_color, direction) + + +def create_gradient_image(width: int, height: int, start_color: Tuple[int, int, int], + end_color: Tuple[int, int, int], direction: str = 'horizontal') -> Image.Image: + """ + Create a gradient image using PIL. + + Args: + width: Image width in pixels + height: Image height in pixels + start_color: Starting RGB color tuple + end_color: Ending RGB color tuple + direction: Gradient direction + + Returns: + PIL Image object with gradient + """ + img = Image.new('RGB', (width, height)) + draw = ImageDraw.Draw(img) + + if direction == 'horizontal': + for x in range(width): + ratio = x / width + r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio) + g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio) + b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio) + draw.line([(x, 0), (x, height)], fill=(r, g, b)) + elif direction == 'vertical': + for y in range(height): + ratio = y / height + r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio) + g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio) + b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio) + draw.line([(0, y), (width, y)], fill=(r, g, b)) + else: # diagonal + for x in range(width): + for y in range(height): + ratio = (x + y) / (width + height) + r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio) + g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio) + b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio) + img.putpixel((x, y), (r, g, b)) + + return img + + +def format_shape(shape, fill_color: Tuple[int, int, int] = None, + line_color: Tuple[int, int, int] = None, line_width: float = None) -> None: + """ + Format a shape with color and line properties. + + Args: + shape: The shape object + fill_color: RGB fill color tuple + line_color: RGB line color tuple + line_width: Line width in points + """ + try: + if fill_color: + shape.fill.solid() + shape.fill.fore_color.rgb = RGBColor(*fill_color) + + if line_color: + shape.line.color.rgb = RGBColor(*line_color) + + if line_width is not None: + shape.line.width = Pt(line_width) + except Exception: + pass # Graceful fallback + + +# Image enhancement functions +def enhance_image_with_pillow(image_path: str, brightness: float = 1.0, contrast: float = 1.0, + saturation: float = 1.0, sharpness: float = 1.0, + blur_radius: float = 0, filter_type: str = None, + output_path: str = None) -> str: + """ + Enhance an image using PIL with various adjustments. + + Args: + image_path: Path to input image + brightness: Brightness factor (1.0 = no change) + contrast: Contrast factor (1.0 = no change) + saturation: Saturation factor (1.0 = no change) + sharpness: Sharpness factor (1.0 = no change) + blur_radius: Blur radius (0 = no blur) + filter_type: Filter type ('BLUR', 'SHARPEN', 'SMOOTH', etc.) + output_path: Output path (if None, generates temporary file) + + Returns: + Path to enhanced image + """ + if not os.path.exists(image_path): + raise FileNotFoundError(f"Image file not found: {image_path}") + + # Open image + img = Image.open(image_path) + + # Apply enhancements + if brightness != 1.0: + enhancer = ImageEnhance.Brightness(img) + img = enhancer.enhance(brightness) + + if contrast != 1.0: + enhancer = ImageEnhance.Contrast(img) + img = enhancer.enhance(contrast) + + if saturation != 1.0: + enhancer = ImageEnhance.Color(img) + img = enhancer.enhance(saturation) + + if sharpness != 1.0: + enhancer = ImageEnhance.Sharpness(img) + img = enhancer.enhance(sharpness) + + if blur_radius > 0: + img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius)) + + if filter_type: + filter_map = { + 'BLUR': ImageFilter.BLUR, + 'SHARPEN': ImageFilter.SHARPEN, + 'SMOOTH': ImageFilter.SMOOTH, + 'EDGE_ENHANCE': ImageFilter.EDGE_ENHANCE + } + if filter_type.upper() in filter_map: + img = img.filter(filter_map[filter_type.upper()]) + + # Save enhanced image + if output_path is None: + output_path = tempfile.mktemp(suffix='.png') + + img.save(output_path) + return output_path + + +def apply_professional_image_enhancement(image_path: str, style: str = 'presentation', + output_path: str = None) -> str: + """ + Apply professional image enhancement presets. + + Args: + image_path: Path to input image + style: Enhancement style ('presentation', 'bright', 'soft') + output_path: Output path (if None, generates temporary file) + + Returns: + Path to enhanced image + """ + enhancement_presets = { + 'presentation': { + 'brightness': 1.1, + 'contrast': 1.15, + 'saturation': 1.1, + 'sharpness': 1.2 + }, + 'bright': { + 'brightness': 1.2, + 'contrast': 1.1, + 'saturation': 1.2, + 'sharpness': 1.1 + }, + 'soft': { + 'brightness': 1.05, + 'contrast': 0.95, + 'saturation': 0.95, + 'sharpness': 0.9, + 'blur_radius': 0.5 + } + } + + preset = enhancement_presets.get(style, enhancement_presets['presentation']) + return enhance_image_with_pillow(image_path, output_path=output_path, **preset) + + +# Picture effects functions (simplified implementations) +def apply_picture_shadow(picture_shape, shadow_type: str = 'outer', blur_radius: float = 4.0, + distance: float = 3.0, direction: float = 315.0, + color: Tuple[int, int, int] = (0, 0, 0), transparency: float = 0.6) -> Dict: + """Apply shadow effect to a picture shape.""" + try: + # Simplified implementation - actual shadow effects require XML manipulation + return {"success": True, "effect": "shadow", "message": "Shadow effect applied"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def apply_picture_reflection(picture_shape, size: float = 0.5, transparency: float = 0.5, + distance: float = 0.0, blur: float = 4.0) -> Dict: + """Apply reflection effect to a picture shape.""" + try: + return {"success": True, "effect": "reflection", "message": "Reflection effect applied"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def apply_picture_glow(picture_shape, size: float = 5.0, color: Tuple[int, int, int] = (0, 176, 240), + transparency: float = 0.4) -> Dict: + """Apply glow effect to a picture shape.""" + try: + return {"success": True, "effect": "glow", "message": "Glow effect applied"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def apply_picture_soft_edges(picture_shape, radius: float = 2.5) -> Dict: + """Apply soft edges effect to a picture shape.""" + try: + return {"success": True, "effect": "soft_edges", "message": "Soft edges effect applied"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def apply_picture_rotation(picture_shape, rotation: float) -> Dict: + """Apply rotation to a picture shape.""" + try: + picture_shape.rotation = rotation + return {"success": True, "effect": "rotation", "message": f"Rotated by {rotation} degrees"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def apply_picture_transparency(picture_shape, transparency: float) -> Dict: + """Apply transparency to a picture shape.""" + try: + return {"success": True, "effect": "transparency", "message": "Transparency applied"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def apply_picture_bevel(picture_shape, bevel_type: str = 'circle', width: float = 6.0, + height: float = 6.0) -> Dict: + """Apply bevel effect to a picture shape.""" + try: + return {"success": True, "effect": "bevel", "message": "Bevel effect applied"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def apply_picture_filter(picture_shape, filter_type: str = 'none', intensity: float = 0.5) -> Dict: + """Apply color filter to a picture shape.""" + try: + return {"success": True, "effect": "filter", "message": f"Applied {filter_type} filter"} + except Exception as e: + return {"success": False, "error": str(e)} + + +# Font management functions +def analyze_font_file(font_path: str) -> Dict: + """ + Analyze a font file using FontTools. + + Args: + font_path: Path to the font file + + Returns: + Dictionary with font analysis results + """ + try: + font = TTFont(font_path) + + # Get basic font information + name_table = font['name'] + font_family = "" + font_style = "" + + for record in name_table.names: + if record.nameID == 1: # Font Family name + font_family = str(record) + elif record.nameID == 2: # Font Subfamily name + font_style = str(record) + + return { + "file_path": font_path, + "font_family": font_family, + "font_style": font_style, + "num_glyphs": font.getGlyphSet().keys().__len__(), + "file_size": os.path.getsize(font_path), + "analysis_success": True + } + except Exception as e: + return { + "file_path": font_path, + "analysis_success": False, + "error": str(e) + } + + +def optimize_font_for_presentation(font_path: str, output_path: str = None, + text_content: str = None) -> str: + """ + Optimize a font file for presentation use. + + Args: + font_path: Path to input font file + output_path: Path for optimized font (if None, generates temporary file) + text_content: Text content to subset for (if None, keeps all characters) + + Returns: + Path to optimized font file + """ + try: + font = TTFont(font_path) + + if text_content: + # Subset font to only include used characters + subsetter = Subsetter() + subsetter.populate(text=text_content) + subsetter.subset(font) + + # Generate output path if not provided + if output_path is None: + output_path = tempfile.mktemp(suffix='.ttf') + + font.save(output_path) + return output_path + except Exception as e: + raise Exception(f"Font optimization failed: {str(e)}") + + +def get_font_recommendations(font_path: str, presentation_type: str = 'business') -> Dict: + """ + Get font usage recommendations. + + Args: + font_path: Path to font file + presentation_type: Type of presentation ('business', 'creative', 'academic') + + Returns: + Dictionary with font recommendations + """ + try: + analysis = analyze_font_file(font_path) + + recommendations = { + "suitable_for": [], + "recommended_sizes": {}, + "usage_tips": [], + "compatibility": "good" + } + + if presentation_type == 'business': + recommendations["suitable_for"] = ["titles", "body_text", "captions"] + recommendations["recommended_sizes"] = { + "title": "24-36pt", + "subtitle": "16-20pt", + "body": "12-16pt" + } + recommendations["usage_tips"] = [ + "Use for professional presentations", + "Good for readability at distance", + "Works well with business themes" + ] + + return { + "font_analysis": analysis, + "presentation_type": presentation_type, + "recommendations": recommendations + } + except Exception as e: + return { + "error": str(e), + "recommendations": None + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/presentation_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/presentation_utils.py new file mode 100644 index 00000000..849d8bb0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/presentation_utils.py @@ -0,0 +1,217 @@ +""" +Presentation management utilities for PowerPoint MCP Server. +Functions for creating, opening, saving, and managing presentations. +""" +from pptx import Presentation +from typing import Dict, List, Optional +import os + + +def create_presentation() -> Presentation: + """ + Create a new PowerPoint presentation. + + Returns: + A new Presentation object + """ + return Presentation() + + +def open_presentation(file_path: str) -> Presentation: + """ + Open an existing PowerPoint presentation. + + Args: + file_path: Path to the PowerPoint file + + Returns: + A Presentation object + """ + return Presentation(file_path) + + +def create_presentation_from_template(template_path: str) -> Presentation: + """ + Create a new PowerPoint presentation from a template file. + + Args: + template_path: Path to the template .pptx file + + Returns: + A new Presentation object based on the template + + Raises: + FileNotFoundError: If the template file doesn't exist + Exception: If the template file is corrupted or invalid + """ + if not os.path.exists(template_path): + raise FileNotFoundError(f"Template file not found: {template_path}") + + if not template_path.lower().endswith(('.pptx', '.potx')): + raise ValueError("Template file must be a .pptx or .potx file") + + try: + # Load the template file as a presentation + presentation = Presentation(template_path) + return presentation + except Exception as e: + raise Exception(f"Failed to load template file '{template_path}': {str(e)}") + + +def save_presentation(presentation: Presentation, file_path: str) -> str: + """ + Save a PowerPoint presentation to a file. + + Args: + presentation: The Presentation object + file_path: Path where the file should be saved + + Returns: + The file path where the presentation was saved + """ + presentation.save(file_path) + return file_path + + +def get_template_info(template_path: str) -> Dict: + """ + Get information about a template file. + + Args: + template_path: Path to the template .pptx file + + Returns: + Dictionary containing template information + """ + if not os.path.exists(template_path): + raise FileNotFoundError(f"Template file not found: {template_path}") + + try: + presentation = Presentation(template_path) + + # Get slide layouts + layouts = get_slide_layouts(presentation) + + # Get core properties + core_props = get_core_properties(presentation) + + # Get slide count + slide_count = len(presentation.slides) + + # Get file size + file_size = os.path.getsize(template_path) + + return { + "template_path": template_path, + "file_size_bytes": file_size, + "slide_count": slide_count, + "layout_count": len(layouts), + "slide_layouts": layouts, + "core_properties": core_props + } + except Exception as e: + raise Exception(f"Failed to read template info from '{template_path}': {str(e)}") + + +def get_presentation_info(presentation: Presentation) -> Dict: + """ + Get information about a presentation. + + Args: + presentation: The Presentation object + + Returns: + Dictionary containing presentation information + """ + try: + # Get slide layouts + layouts = get_slide_layouts(presentation) + + # Get core properties + core_props = get_core_properties(presentation) + + # Get slide count + slide_count = len(presentation.slides) + + return { + "slide_count": slide_count, + "layout_count": len(layouts), + "slide_layouts": layouts, + "core_properties": core_props, + "slide_width": presentation.slide_width, + "slide_height": presentation.slide_height + } + except Exception as e: + raise Exception(f"Failed to get presentation info: {str(e)}") + + +def get_slide_layouts(presentation: Presentation) -> List[Dict]: + """ + Get all available slide layouts in the presentation. + + Args: + presentation: The Presentation object + + Returns: + A list of dictionaries with layout information + """ + layouts = [] + for i, layout in enumerate(presentation.slide_layouts): + layout_info = { + "index": i, + "name": layout.name, + "placeholder_count": len(layout.placeholders) + } + layouts.append(layout_info) + return layouts + + +def set_core_properties(presentation: Presentation, title: str = None, subject: str = None, + author: str = None, keywords: str = None, comments: str = None) -> None: + """ + Set core document properties. + + Args: + presentation: The Presentation object + title: Document title + subject: Document subject + author: Document author + keywords: Document keywords + comments: Document comments + """ + core_props = presentation.core_properties + + if title is not None: + core_props.title = title + if subject is not None: + core_props.subject = subject + if author is not None: + core_props.author = author + if keywords is not None: + core_props.keywords = keywords + if comments is not None: + core_props.comments = comments + + +def get_core_properties(presentation: Presentation) -> Dict: + """ + Get core document properties. + + Args: + presentation: The Presentation object + + Returns: + Dictionary containing core properties + """ + core_props = presentation.core_properties + + return { + "title": core_props.title, + "subject": core_props.subject, + "author": core_props.author, + "keywords": core_props.keywords, + "comments": core_props.comments, + "created": core_props.created.isoformat() if core_props.created else None, + "last_modified_by": core_props.last_modified_by, + "modified": core_props.modified.isoformat() if core_props.modified else None + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/template_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/template_utils.py new file mode 100644 index 00000000..ebd1f532 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/template_utils.py @@ -0,0 +1,1143 @@ +""" +Enhanced template management utilities for PowerPoint MCP Server. +Advanced slide creation with dynamic sizing, auto-wrapping, and visual effects. +Combines features from both basic and enhanced template systems. +""" +import json +import os +import re +from typing import Dict, List, Optional, Any, Tuple +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_VERTICAL_ANCHOR +from pptx.enum.shapes import MSO_SHAPE +import utils.content_utils as content_utils +import utils.design_utils as design_utils + + +class TextSizeCalculator: + """Calculate optimal text sizes based on content and container dimensions.""" + + def __init__(self): + self.character_widths = { + 'narrow': 0.6, # i, l, t + 'normal': 1.0, # most characters + 'wide': 1.3, # m, w + 'space': 0.5 # space character + } + + def estimate_text_width(self, text: str, font_size: int) -> float: + """Estimate text width in points based on character analysis.""" + if not text: + return 0 + + width = 0 + for char in text: + if char in 'iltj': + width += self.character_widths['narrow'] + elif char in 'mwMW': + width += self.character_widths['wide'] + elif char == ' ': + width += self.character_widths['space'] + else: + width += self.character_widths['normal'] + + return width * font_size * 0.6 # Approximation factor + + def estimate_text_height(self, text: str, font_size: int, line_spacing: float = 1.2) -> float: + """Estimate text height based on line count and spacing.""" + lines = len(text.split('\n')) + return lines * font_size * line_spacing * 1.3 # Convert to points + + def calculate_optimal_font_size(self, text: str, container_width: float, + container_height: float, font_type: str = 'body', + min_size: int = 8, max_size: int = 36) -> int: + """Calculate optimal font size to fit text in container.""" + container_width_pts = container_width * 72 # Convert inches to points + container_height_pts = container_height * 72 + + # Start with a reasonable size and adjust + for font_size in range(max_size, min_size - 1, -1): + estimated_width = self.estimate_text_width(text, font_size) + estimated_height = self.estimate_text_height(text, font_size) + + if estimated_width <= container_width_pts * 0.9 and estimated_height <= container_height_pts * 0.9: + return font_size + + return min_size + + def wrap_text_intelligently(self, text: str, max_width: float, font_size: int) -> str: + """Intelligently wrap text to fit within specified width.""" + if not text: + return text + + max_width_pts = max_width * 72 + words = text.split() + wrapped_lines = [] + current_line = [] + + for word in words: + test_line = current_line + [word] + test_text = ' '.join(test_line) + + if self.estimate_text_width(test_text, font_size) <= max_width_pts: + current_line.append(word) + else: + if current_line: + wrapped_lines.append(' '.join(current_line)) + current_line = [word] + else: + # Single word is too long, force wrap + wrapped_lines.append(word) + + if current_line: + wrapped_lines.append(' '.join(current_line)) + + return '\n'.join(wrapped_lines) + + +class VisualEffectsManager: + """Manage and apply visual effects to PowerPoint elements.""" + + def __init__(self, templates_data: Dict): + self.templates_data = templates_data + self.text_effects = templates_data.get('text_effects', {}) + self.image_effects = templates_data.get('image_effects', {}) + + def apply_text_effects(self, text_frame, effects: List[str], color_scheme: str) -> None: + """Apply text effects like shadows, glows, and outlines.""" + for effect_name in effects: + if effect_name not in self.text_effects: + continue + + effect_config = self.text_effects[effect_name] + effect_type = effect_config.get('type') + + # Note: These are simplified implementations + # Full implementation would require XML manipulation + try: + if effect_type == 'shadow': + self._apply_text_shadow(text_frame, effect_config, color_scheme) + elif effect_type == 'glow': + self._apply_text_glow(text_frame, effect_config, color_scheme) + elif effect_type == 'outline': + self._apply_text_outline(text_frame, effect_config, color_scheme) + except Exception: + # Graceful fallback if effect application fails + pass + + def _apply_text_shadow(self, text_frame, config: Dict, color_scheme: str) -> None: + """Apply shadow effect to text (simplified implementation).""" + # In a full implementation, this would manipulate the XML directly + # For now, we'll apply basic formatting that creates a shadow-like effect + for paragraph in text_frame.paragraphs: + for run in paragraph.runs: + # Make text slightly bolder to simulate shadow depth + run.font.bold = True + + def _apply_text_glow(self, text_frame, config: Dict, color_scheme: str) -> None: + """Apply glow effect to text (simplified implementation).""" + pass # Would require XML manipulation for true glow effect + + def _apply_text_outline(self, text_frame, config: Dict, color_scheme: str) -> None: + """Apply outline effect to text (simplified implementation).""" + pass # Would require XML manipulation for true outline effect + + def apply_image_effects(self, image_shape, effect_name: str, color_scheme: str) -> None: + """Apply visual effects to image shapes.""" + if effect_name not in self.image_effects: + return + + effect_config = self.image_effects[effect_name] + + try: + # Apply shadow if specified + if 'shadow' in effect_config: + shadow_config = effect_config['shadow'] + # Simplified shadow application + pass + + # Apply border if specified + if 'border' in effect_config: + border_config = effect_config['border'] + if 'width' in border_config: + image_shape.line.width = Pt(border_config['width']) + if 'color_role' in border_config: + color = self._get_color_from_scheme(color_scheme, border_config['color_role']) + image_shape.line.color.rgb = RGBColor(*color) + elif 'color' in border_config: + image_shape.line.color.rgb = RGBColor(*border_config['color']) + + except Exception: + # Graceful fallback + pass + + def _get_color_from_scheme(self, color_scheme: str, color_role: str) -> Tuple[int, int, int]: + """Get color from scheme (helper method).""" + schemes = self.templates_data.get('color_schemes', {}) + if color_scheme in schemes and color_role in schemes[color_scheme]: + return tuple(schemes[color_scheme][color_role]) + return (0, 0, 0) # Default black + + +class EnhancedTemplateManager: + """Enhanced template manager with dynamic features.""" + + def __init__(self, template_file_path: str = None): + self.text_calculator = TextSizeCalculator() + self.load_templates(template_file_path) + self.effects_manager = VisualEffectsManager(self.templates_data) + + def load_templates(self, template_file_path: str = None) -> None: + """Load unified templates with all dynamic features.""" + if template_file_path is None: + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + # Use the unified template file + template_file_path = os.path.join(current_dir, 'slide_layout_templates.json') + + try: + with open(template_file_path, 'r', encoding='utf-8') as f: + self.templates_data = json.load(f) + except FileNotFoundError: + raise FileNotFoundError(f"Template file not found: {template_file_path}") + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in template file: {str(e)}") + + + def get_dynamic_font_size(self, element: Dict, content: str = None) -> int: + """Calculate dynamic font size based on content and container.""" + content = content or element.get('placeholder_text', '') + if not content: + return 14 # Default size + + # Get container dimensions + pos = element.get('position', {}) + container_width = pos.get('width', 4.0) + container_height = pos.get('height', 1.0) + + # Get font constraints + font_type = element.get('styling', {}).get('font_type', 'body') + sizing_rules = self.templates_data.get('auto_sizing_rules', {}) + base_sizes = sizing_rules.get('text_measurement', {}).get('base_font_sizes', {}) + + if font_type in base_sizes: + min_size = base_sizes[font_type]['min'] + max_size = base_sizes[font_type]['max'] + default_size = base_sizes[font_type]['default'] + else: + min_size, max_size, default_size = 10, 18, 14 + + # Check if dynamic sizing is requested + font_size_setting = element.get('styling', {}).get('font_size') + if font_size_setting == 'dynamic': + return self.text_calculator.calculate_optimal_font_size( + content, container_width, container_height, font_type, min_size, max_size + ) + + return default_size + + def apply_enhanced_slide_template(self, slide, template_id: str, color_scheme: str = 'modern_blue', + content_mapping: Dict = None, image_paths: Dict = None) -> Dict: + """Apply enhanced slide template with all dynamic features.""" + try: + if template_id not in self.templates_data.get('templates', {}): + # Fall back to regular template application + return apply_slide_template_basic(slide, template_id, color_scheme, content_mapping, image_paths) + + template = self.templates_data['templates'][template_id] + elements_created = [] + + # Apply enhanced background if specified + background_config = template.get('background') + if background_config: + apply_slide_background(slide, background_config, self.templates_data, color_scheme) + + # Create enhanced elements + for element in template.get('elements', []): + element_type = element.get('type') + element_role = element.get('role', '') + + try: + # Override content if provided + custom_content = None + if content_mapping and element_role in content_mapping: + custom_content = content_mapping[element_role] + + created_element = None + + if element_type == 'text': + created_element = self.create_enhanced_text_element( + slide, element, self.templates_data, color_scheme, custom_content + ) + elif element_type == 'shape': + created_element = create_shape_element(slide, element, self.templates_data, color_scheme) + elif element_type == 'image': + image_path = image_paths.get(element_role) if image_paths else None + created_element = create_image_element(slide, element, image_path) + elif element_type == 'table': + created_element = create_table_element(slide, element, self.templates_data, color_scheme) + elif element_type == 'chart': + created_element = create_chart_element(slide, element, self.templates_data, color_scheme) + + if created_element: + elements_created.append({ + 'type': element_type, + 'role': element_role, + 'index': len(slide.shapes) - 1, + 'enhanced_features': self.get_element_features(element) + }) + + except Exception as e: + elements_created.append({ + 'type': element_type, + 'role': element_role, + 'error': str(e) + }) + + return { + 'success': True, + 'template_id': template_id, + 'template_name': template.get('name', template_id), + 'color_scheme': color_scheme, + 'elements_created': elements_created, + 'enhanced_features_applied': [ + 'Dynamic text sizing', + 'Automatic text wrapping', + 'Visual effects', + 'Intelligent content adaptation' + ] + } + + except Exception as e: + return { + 'success': False, + 'error': f"Failed to apply enhanced template: {str(e)}" + } + + def create_enhanced_text_element(self, slide, element: Dict, templates_data: Dict, + color_scheme: str, custom_content: str = None) -> Any: + """Create text element with enhanced features.""" + pos = element['position'] + + # Determine content + content = custom_content or element.get('placeholder_text', '') + + # Apply auto-wrapping if enabled + styling = element.get('styling', {}) + if styling.get('auto_wrap', False): + container_width = pos.get('width', 4.0) + font_size = self.get_dynamic_font_size(element, content) + content = self.text_calculator.wrap_text_intelligently(content, container_width, font_size) + + # Create text box + textbox = slide.shapes.add_textbox( + Inches(pos['left']), + Inches(pos['top']), + Inches(pos['width']), + Inches(pos['height']) + ) + + textbox.text_frame.text = content + textbox.text_frame.word_wrap = True + + # Apply dynamic font sizing + font_size = self.get_dynamic_font_size(element, content) + + # Apply enhanced styling + self.apply_enhanced_text_styling(textbox.text_frame, element, templates_data, color_scheme, font_size) + + # Apply auto-fit if enabled + if styling.get('auto_fit', False): + textbox.text_frame.auto_size = True + + return textbox + + def apply_enhanced_text_styling(self, text_frame, element: Dict, templates_data: Dict, + color_scheme: str, font_size: int) -> None: + """Apply enhanced text styling with effects and dynamic features.""" + styling = element.get('styling', {}) + + # Get typography style + typography_style = templates_data.get('typography_styles', {}).get('modern_sans', {}) + font_type = styling.get('font_type', 'body') + font_config = typography_style.get(font_type, {'name': 'Segoe UI', 'weight': 'normal'}) + + # Color handling + color = None + if 'color_role' in styling: + color = get_color_from_scheme(templates_data, color_scheme, styling['color_role']) + elif 'color' in styling: + color = tuple(styling['color']) + + # Alignment mapping + alignment_map = { + 'left': PP_ALIGN.LEFT, + 'center': PP_ALIGN.CENTER, + 'right': PP_ALIGN.RIGHT, + 'justify': PP_ALIGN.JUSTIFY + } + + # Vertical alignment mapping + vertical_alignment_map = { + 'top': MSO_VERTICAL_ANCHOR.TOP, + 'middle': MSO_VERTICAL_ANCHOR.MIDDLE, + 'bottom': MSO_VERTICAL_ANCHOR.BOTTOM + } + + # Apply vertical alignment to text frame + if 'vertical_alignment' in styling: + v_align = styling['vertical_alignment'] + if v_align in vertical_alignment_map: + text_frame.vertical_anchor = vertical_alignment_map[v_align] + + # Dynamic line spacing + line_spacing = styling.get('line_spacing', 1.2) + if line_spacing == 'dynamic': + content_length = len(text_frame.text) + if content_length > 300: + line_spacing = 1.4 + elif content_length > 150: + line_spacing = 1.3 + else: + line_spacing = 1.2 + + # Apply formatting to paragraphs and runs + for paragraph in text_frame.paragraphs: + # Set alignment + if 'alignment' in styling and styling['alignment'] in alignment_map: + paragraph.alignment = alignment_map[styling['alignment']] + + # Set line spacing + paragraph.line_spacing = line_spacing + + # Apply formatting to runs + for run in paragraph.runs: + font = run.font + + # Font family and size + font.name = font_config['name'] + font.size = Pt(font_size) + + # Font weight and style + weight = font_config.get('weight', 'normal') + font.bold = styling.get('bold', weight in ['bold', 'semibold']) + font.italic = styling.get('italic', font_config.get('style') == 'italic') + font.underline = styling.get('underline', False) + + # Color + if color: + font.color.rgb = RGBColor(*color) + + # Apply text effects + text_effects = styling.get('text_effects', []) + if text_effects: + self.effects_manager.apply_text_effects(text_frame, text_effects, color_scheme) + + def get_element_features(self, element: Dict) -> List[str]: + """Get list of enhanced features applied to an element.""" + features = [] + styling = element.get('styling', {}) + + if styling.get('font_size') == 'dynamic': + features.append('Dynamic text sizing') + if styling.get('auto_wrap'): + features.append('Automatic text wrapping') + if styling.get('text_effects'): + features.append('Text visual effects') + if styling.get('auto_fit'): + features.append('Auto-fit content') + if 'fill_gradient' in styling: + features.append('Gradient fills') + if styling.get('shadow') or styling.get('glow'): + features.append('Advanced visual effects') + + return features + + +# Global instance for enhanced features +enhanced_template_manager = EnhancedTemplateManager() + + +def get_enhanced_template_manager() -> EnhancedTemplateManager: + """Get the global enhanced template manager instance.""" + return enhanced_template_manager + + +def calculate_dynamic_font_size(text: str, container_width: float, container_height: float, + font_type: str = 'body') -> int: + """Calculate optimal font size for given text and container.""" + return enhanced_template_manager.text_calculator.calculate_optimal_font_size( + text, container_width, container_height, font_type + ) + + +def wrap_text_automatically(text: str, container_width: float, font_size: int) -> str: + """Automatically wrap text to fit container width.""" + return enhanced_template_manager.text_calculator.wrap_text_intelligently( + text, container_width, font_size + ) + + +def load_slide_templates(template_file_path: str = None) -> Dict: + """ + Load slide layout templates from JSON file. + + Args: + template_file_path: Path to template JSON file (defaults to slide_layout_templates.json) + + Returns: + Dictionary containing all template definitions + """ + if template_file_path is None: + # Default to the template file in the same directory as the script + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + template_file_path = os.path.join(current_dir, 'slide_layout_templates.json') + + try: + with open(template_file_path, 'r', encoding='utf-8') as f: + templates = json.load(f) + return templates + except FileNotFoundError: + raise FileNotFoundError(f"Template file not found: {template_file_path}") + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in template file: {str(e)}") + + +def get_available_templates() -> List[Dict]: + """ + Get a list of all available slide templates. + + Returns: + List of template information dictionaries + """ + try: + templates_data = load_slide_templates() + template_list = [] + + for template_id, template_info in templates_data.get('templates', {}).items(): + template_list.append({ + 'id': template_id, + 'name': template_info.get('name', template_id), + 'description': template_info.get('description', ''), + 'layout_type': template_info.get('layout_type', 'content'), + 'element_count': len(template_info.get('elements', [])) + }) + + return template_list + except Exception as e: + return [{'error': f"Failed to load templates: {str(e)}"}] + + +def get_color_from_scheme(templates_data: Dict, color_scheme: str, color_role: str) -> Tuple[int, int, int]: + """ + Get RGB color values from a color scheme. + + Args: + templates_data: Template data dictionary + color_scheme: Name of the color scheme + color_role: Role of the color (primary, secondary, accent1, etc.) + + Returns: + RGB color tuple (r, g, b) + """ + color_schemes = templates_data.get('color_schemes', {}) + + if color_scheme not in color_schemes: + color_scheme = 'modern_blue' # Default fallback + + scheme = color_schemes[color_scheme] + return tuple(scheme.get(color_role, scheme.get('primary', [0, 120, 215]))) + + +def get_font_settings(templates_data: Dict, font_type: str, font_size: str) -> Dict: + """ + Get font settings from typography configuration. + + Args: + templates_data: Template data dictionary + font_type: Type of font (title, subtitle, body, caption) + font_size: Size category (large, medium, small) + + Returns: + Dictionary with font settings + """ + typography = templates_data.get('typography', {}) + + if font_type not in typography: + font_type = 'body' # Default fallback + + font_config = typography[font_type] + size_key = f'font_size_{font_size}' + + return { + 'name': font_config.get('font_name', 'Segoe UI'), + 'size': font_config.get(size_key, font_config.get('font_size_medium', 14)), + 'bold': font_config.get('bold', False) + } + + +def apply_text_styling(text_frame, styling: Dict, templates_data: Dict, color_scheme: str) -> None: + """ + Apply text styling based on template configuration. + + Args: + text_frame: PowerPoint text frame object + styling: Styling configuration from template + templates_data: Template data dictionary + color_scheme: Selected color scheme + """ + # Get font settings + font_type = styling.get('font_type', 'body') + font_size_category = styling.get('font_size', 'medium') + font_settings = get_font_settings(templates_data, font_type, font_size_category) + + # Get color + color = None + if 'color_role' in styling: + color = get_color_from_scheme(templates_data, color_scheme, styling['color_role']) + elif 'color' in styling: + color = tuple(styling['color']) + + # Apply alignment + alignment_map = { + 'left': PP_ALIGN.LEFT, + 'center': PP_ALIGN.CENTER, + 'right': PP_ALIGN.RIGHT, + 'justify': PP_ALIGN.JUSTIFY + } + + # Apply formatting to all paragraphs and runs + for paragraph in text_frame.paragraphs: + if 'alignment' in styling and styling['alignment'] in alignment_map: + paragraph.alignment = alignment_map[styling['alignment']] + + for run in paragraph.runs: + font = run.font + font.name = font_settings['name'] + font.size = Pt(font_settings['size']) + font.bold = styling.get('bold', font_settings['bold']) + font.italic = styling.get('italic', False) + font.underline = styling.get('underline', False) + + if color: + font.color.rgb = RGBColor(*color) + + +def create_text_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any: + """ + Create a text element on a slide based on template configuration. + + Args: + slide: PowerPoint slide object + element: Element configuration from template + templates_data: Template data dictionary + color_scheme: Selected color scheme + + Returns: + Created text box shape + """ + pos = element['position'] + textbox = slide.shapes.add_textbox( + Inches(pos['left']), + Inches(pos['top']), + Inches(pos['width']), + Inches(pos['height']) + ) + + # Set text content + textbox.text_frame.text = element.get('placeholder_text', '') + + # Apply styling + styling = element.get('styling', {}) + apply_text_styling(textbox.text_frame, styling, templates_data, color_scheme) + + return textbox + + +def create_image_element(slide, element: Dict, image_path: str = None) -> Any: + """ + Create an image element on a slide based on template configuration. + + Args: + slide: PowerPoint slide object + element: Element configuration from template + image_path: Optional path to image file + + Returns: + Created image shape or None if no image provided + """ + if not image_path: + # Create placeholder rectangle if no image provided + pos = element['position'] + placeholder = slide.shapes.add_shape( + 1, # Rectangle shape + Inches(pos['left']), + Inches(pos['top']), + Inches(pos['width']), + Inches(pos['height']) + ) + + # Add placeholder text + if hasattr(placeholder, 'text_frame'): + placeholder.text_frame.text = element.get('placeholder_text', 'Image Placeholder') + + return placeholder + + try: + pos = element['position'] + image_shape = content_utils.add_image( + slide, + image_path, + pos['left'], + pos['top'], + pos['width'], + pos['height'] + ) + + # Apply styling if specified + styling = element.get('styling', {}) + if styling.get('shadow'): + # Apply shadow effect (simplified) + pass + + return image_shape + except Exception: + # Fallback to placeholder if image fails to load + return create_image_element(slide, element, None) + + +def create_shape_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any: + """ + Create a shape element on a slide based on template configuration. + + Args: + slide: PowerPoint slide object + element: Element configuration from template + templates_data: Template data dictionary + color_scheme: Selected color scheme + + Returns: + Created shape + """ + pos = element['position'] + shape_type = element.get('shape_type', 'rectangle') + + try: + # Import the shape creation function from the main server + from ppt_mcp_server import add_shape_direct + shape = add_shape_direct(slide, shape_type, pos['left'], pos['top'], pos['width'], pos['height']) + + # Apply styling + styling = element.get('styling', {}) + + # Fill color + if 'fill_color_role' in styling: + fill_color = get_color_from_scheme(templates_data, color_scheme, styling['fill_color_role']) + shape.fill.solid() + shape.fill.fore_color.rgb = RGBColor(*fill_color) + elif 'fill_color' in styling: + shape.fill.solid() + shape.fill.fore_color.rgb = RGBColor(*styling['fill_color']) + + # Line color + if 'line_color_role' in styling: + line_color = get_color_from_scheme(templates_data, color_scheme, styling['line_color_role']) + shape.line.color.rgb = RGBColor(*line_color) + elif styling.get('no_border'): + shape.line.fill.background() + + # Transparency + if 'transparency' in styling: + # Note: Transparency implementation would need additional XML manipulation + pass + + return shape + except Exception as e: + # Create a simple rectangle as fallback + textbox = slide.shapes.add_textbox( + Inches(pos['left']), + Inches(pos['top']), + Inches(pos['width']), + Inches(pos['height']) + ) + textbox.text_frame.text = f"Shape: {shape_type}" + return textbox + + +def create_table_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any: + """ + Create a table element on a slide based on template configuration. + + Args: + slide: PowerPoint slide object + element: Element configuration from template + templates_data: Template data dictionary + color_scheme: Selected color scheme + + Returns: + Created table shape + """ + pos = element['position'] + table_config = element.get('table_config', {}) + + rows = table_config.get('rows', 3) + cols = table_config.get('cols', 3) + + # Create table + table_shape = content_utils.add_table( + slide, rows, cols, pos['left'], pos['top'], pos['width'], pos['height'] + ) + table = table_shape.table + + # Populate with data if provided + data = table_config.get('data', []) + for r in range(min(rows, len(data))): + for c in range(min(cols, len(data[r]))): + table.cell(r, c).text = str(data[r][c]) + + # Apply styling + styling = element.get('styling', {}) + header_row = table_config.get('header_row', True) + + for r in range(rows): + for c in range(cols): + cell = table.cell(r, c) + + if r == 0 and header_row: + # Header styling + if 'header_bg_color_role' in styling: + bg_color = get_color_from_scheme(templates_data, color_scheme, styling['header_bg_color_role']) + cell.fill.solid() + cell.fill.fore_color.rgb = RGBColor(*bg_color) + + # Header text color + if 'header_text_color' in styling: + for paragraph in cell.text_frame.paragraphs: + for run in paragraph.runs: + run.font.color.rgb = RGBColor(*styling['header_text_color']) + run.font.bold = True + else: + # Body styling + if 'body_bg_color_role' in styling: + bg_color = get_color_from_scheme(templates_data, color_scheme, styling['body_bg_color_role']) + cell.fill.solid() + cell.fill.fore_color.rgb = RGBColor(*bg_color) + + return table_shape + + +def create_chart_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any: + """ + Create a chart element on a slide based on template configuration. + + Args: + slide: PowerPoint slide object + element: Element configuration from template + templates_data: Template data dictionary + color_scheme: Selected color scheme + + Returns: + Created chart object + """ + pos = element['position'] + chart_config = element.get('chart_config', {}) + + chart_type = chart_config.get('type', 'column') + categories = chart_config.get('categories', ['A', 'B', 'C']) + series_data = chart_config.get('series', [{'name': 'Series 1', 'values': [1, 2, 3]}]) + + # Extract series names and values + series_names = [s['name'] for s in series_data] + series_values = [s['values'] for s in series_data] + + try: + # Create chart + chart = content_utils.add_chart( + slide, chart_type, pos['left'], pos['top'], pos['width'], pos['height'], + categories, series_names, series_values + ) + + # Apply formatting + chart_title = chart_config.get('title') + if chart_title: + content_utils.format_chart(chart, title=chart_title) + + return chart + except Exception as e: + # Create placeholder if chart creation fails + textbox = slide.shapes.add_textbox( + Inches(pos['left']), + Inches(pos['top']), + Inches(pos['width']), + Inches(pos['height']) + ) + textbox.text_frame.text = f"Chart: {chart_type}\n{chart_title or 'Chart Placeholder'}" + return textbox + + +def apply_slide_background(slide, background_config: Dict, templates_data: Dict, color_scheme: str) -> None: + """ + Apply background styling to a slide based on template configuration. + + Args: + slide: PowerPoint slide object + background_config: Background configuration from template + templates_data: Template data dictionary + color_scheme: Selected color scheme + """ + if not background_config: + return + + bg_type = background_config.get('type', 'solid') + + if bg_type == 'professional_gradient': + style = background_config.get('style', 'subtle') + direction = background_config.get('direction', 'diagonal') + design_utils.create_professional_gradient_background(slide, color_scheme, style, direction) + elif bg_type == 'solid': + color_role = background_config.get('color_role', 'light') + # Note: Solid background would require XML manipulation for proper implementation + pass + + + + +def apply_slide_template_basic(slide, template_id: str, color_scheme: str = 'modern_blue', + content_mapping: Dict = None, image_paths: Dict = None) -> Dict: + """ + Apply a basic slide template to create a formatted slide. + + Args: + slide: PowerPoint slide object + template_id: ID of the template to apply + color_scheme: Color scheme to use + content_mapping: Dictionary mapping element roles to content + image_paths: Dictionary mapping image element roles to file paths + + Returns: + Dictionary with application results + """ + try: + # Load templates + templates_data = load_slide_templates() + + if template_id not in templates_data.get('templates', {}): + return { + 'success': False, + 'error': f"Template '{template_id}' not found" + } + + template = templates_data['templates'][template_id] + elements_created = [] + + # Apply background if specified + background_config = template.get('background') + if background_config: + apply_slide_background(slide, background_config, templates_data, color_scheme) + + # Create elements + for element in template.get('elements', []): + element_type = element.get('type') + element_role = element.get('role', '') + + try: + # Override placeholder text with custom content if provided + if content_mapping and element_role in content_mapping: + element = element.copy() # Don't modify original template + element['placeholder_text'] = content_mapping[element_role] + + created_element = None + + if element_type == 'text': + created_element = create_text_element(slide, element, templates_data, color_scheme) + elif element_type == 'image': + image_path = image_paths.get(element_role) if image_paths else None + created_element = create_image_element(slide, element, image_path) + elif element_type == 'shape': + created_element = create_shape_element(slide, element, templates_data, color_scheme) + elif element_type == 'table': + created_element = create_table_element(slide, element, templates_data, color_scheme) + elif element_type == 'chart': + created_element = create_chart_element(slide, element, templates_data, color_scheme) + + if created_element: + elements_created.append({ + 'type': element_type, + 'role': element_role, + 'index': len(slide.shapes) - 1 + }) + + except Exception as e: + # Continue with other elements if one fails + elements_created.append({ + 'type': element_type, + 'role': element_role, + 'error': str(e) + }) + + return { + 'success': True, + 'template_id': template_id, + 'template_name': template.get('name', template_id), + 'color_scheme': color_scheme, + 'elements_created': elements_created, + 'total_elements': len(template.get('elements', [])) + } + + except Exception as e: + return { + 'success': False, + 'error': f"Failed to apply template: {str(e)}" + } + + +def apply_slide_template(slide, template_id: str, color_scheme: str = 'modern_blue', + content_mapping: Dict = None, image_paths: Dict = None) -> Dict: + """ + Apply a slide template with all enhanced features. + + Args: + slide: PowerPoint slide object + template_id: ID of the template to apply + color_scheme: Color scheme to use + content_mapping: Dictionary mapping element roles to content + image_paths: Dictionary mapping image element roles to file paths + + Returns: + Dictionary with application results + """ + # All templates now have enhanced features built-in + return enhanced_template_manager.apply_enhanced_slide_template( + slide, template_id, color_scheme, content_mapping, image_paths + ) + + +def create_presentation_from_template_sequence(presentation: Presentation, template_sequence: List[Dict], + color_scheme: str = 'modern_blue') -> Dict: + """ + Create a complete presentation from a sequence of templates. + + Args: + presentation: PowerPoint presentation object + template_sequence: List of template configurations + color_scheme: Color scheme to apply to all slides + + Returns: + Dictionary with creation results + """ + results = { + 'success': True, + 'slides_created': [], + 'total_slides': len(template_sequence), + 'color_scheme': color_scheme + } + + for i, slide_config in enumerate(template_sequence): + try: + # Get template configuration + template_id = slide_config.get('template_id') + content_mapping = slide_config.get('content', {}) + image_paths = slide_config.get('images', {}) + + if not template_id: + results['slides_created'].append({ + 'slide_index': i, + 'success': False, + 'error': 'No template_id specified' + }) + continue + + # Add new slide (using layout 1 as default content layout) + layout = presentation.slide_layouts[1] + slide = presentation.slides.add_slide(layout) + + # Apply template + template_result = apply_slide_template( + slide, template_id, color_scheme, content_mapping, image_paths + ) + + template_result['slide_index'] = i + results['slides_created'].append(template_result) + + if not template_result['success']: + results['success'] = False + + except Exception as e: + results['slides_created'].append({ + 'slide_index': i, + 'success': False, + 'error': f"Failed to create slide {i}: {str(e)}" + }) + results['success'] = False + + return results + + +def get_template_usage_examples() -> Dict: + """ + Get examples of how to use different templates. + + Returns: + Dictionary with usage examples + """ + return { + "single_slide_example": { + "description": "Apply a single template to a slide", + "code": { + "template_id": "text_with_image", + "color_scheme": "modern_blue", + "content_mapping": { + "title": "Our Solution", + "content": "• Increased efficiency by 40%\n• Reduced costs significantly\n• Improved user satisfaction", + }, + "image_paths": { + "supporting": "/path/to/solution_image.jpg" + } + } + }, + "presentation_sequence_example": { + "description": "Create a complete presentation from templates", + "code": [ + { + "template_id": "title_slide", + "content": { + "title": "2024 Business Review", + "subtitle": "Annual Performance Report", + "author": "John Smith, CEO" + } + }, + { + "template_id": "agenda_slide", + "content": { + "agenda_items": "1. Executive Summary\n\n2. Financial Performance\n\n3. Market Analysis\n\n4. Future Strategy" + } + }, + { + "template_id": "key_metrics_dashboard", + "content": { + "metric_1_value": "92%", + "metric_2_value": "$3.2M", + "metric_3_value": "340", + "metric_4_value": "18%" + } + }, + { + "template_id": "thank_you_slide", + "content": { + "contact": "Questions?\njohn.smith@company.com\n(555) 123-4567" + } + } + ] + }, + "available_templates": [ + "title_slide", "text_with_image", "two_column_text", "two_column_text_images", + "three_column_layout", "agenda_slide", "chapter_intro", "thank_you_slide", + "timeline_slide", "data_table_slide", "chart_comparison", "full_image_slide", + "process_flow", "quote_testimonial", "key_metrics_dashboard", + "before_after_comparison", "team_introduction" + ], + "color_schemes": [ + "modern_blue", "corporate_gray", "elegant_green", "warm_red" + ] + } \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/validation_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/validation_utils.py new file mode 100644 index 00000000..2ccaa175 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/validation_utils.py @@ -0,0 +1,323 @@ +""" +Validation utilities for PowerPoint MCP Server. +Functions for validating and fixing slide content, text fit, and layouts. +""" +from typing import Dict, List, Optional, Any + + +def validate_text_fit(shape, text_content: str = None, font_size: int = 12) -> Dict: + """ + Validate if text content will fit in a shape container. + + Args: + shape: The shape containing the text + text_content: The text to validate (if None, uses existing text) + font_size: The font size to check + + Returns: + Dictionary with validation results and suggestions + """ + result = { + 'fits': True, + 'estimated_overflow': False, + 'suggested_font_size': font_size, + 'suggested_dimensions': None, + 'warnings': [], + 'needs_optimization': False + } + + try: + # Use existing text if not provided + if text_content is None and hasattr(shape, 'text_frame'): + text_content = shape.text_frame.text + + if not text_content: + return result + + # Basic heuristic: estimate if text will overflow + if hasattr(shape, 'width') and hasattr(shape, 'height'): + # Rough estimation: average character width is about 0.6 * font_size + avg_char_width = font_size * 0.6 + estimated_width = len(text_content) * avg_char_width + + # Convert shape dimensions to points (assuming they're in EMU) + shape_width_pt = shape.width / 12700 # EMU to points conversion + shape_height_pt = shape.height / 12700 + + if estimated_width > shape_width_pt: + result['fits'] = False + result['estimated_overflow'] = True + result['needs_optimization'] = True + + # Suggest smaller font size + suggested_size = int((shape_width_pt / len(text_content)) * 0.8) + result['suggested_font_size'] = max(suggested_size, 8) + + # Suggest larger dimensions + result['suggested_dimensions'] = { + 'width': estimated_width * 1.2, + 'height': shape_height_pt + } + + result['warnings'].append( + f"Text may overflow. Consider font size {result['suggested_font_size']} " + f"or increase width to {result['suggested_dimensions']['width']:.1f} points" + ) + + # Check for very long lines that might cause formatting issues + lines = text_content.split('\n') + max_line_length = max(len(line) for line in lines) if lines else 0 + + if max_line_length > 100: # Arbitrary threshold + result['warnings'].append("Very long lines detected. Consider adding line breaks.") + result['needs_optimization'] = True + + return result + + except Exception as e: + result['fits'] = False + result['error'] = str(e) + return result + + +def validate_and_fix_slide(slide, auto_fix: bool = True, min_font_size: int = 8, + max_font_size: int = 72) -> Dict: + """ + Comprehensively validate and automatically fix slide content issues. + + Args: + slide: The slide object to validate + auto_fix: Whether to automatically apply fixes + min_font_size: Minimum allowed font size + max_font_size: Maximum allowed font size + + Returns: + Dictionary with validation results and applied fixes + """ + result = { + 'validation_passed': True, + 'issues_found': [], + 'fixes_applied': [], + 'warnings': [], + 'shapes_processed': 0, + 'text_shapes_optimized': 0 + } + + try: + shapes_with_text = [] + + # Find all shapes with text content + for i, shape in enumerate(slide.shapes): + result['shapes_processed'] += 1 + + if hasattr(shape, 'text_frame') and shape.text_frame.text.strip(): + shapes_with_text.append((i, shape)) + + # Validate each text shape + for shape_index, shape in shapes_with_text: + shape_name = f"Shape {shape_index}" + + # Validate text fit + text_validation = validate_text_fit(shape, font_size=12) + + if not text_validation['fits'] or text_validation['needs_optimization']: + issue = f"{shape_name}: Text may not fit properly" + result['issues_found'].append(issue) + result['validation_passed'] = False + + if auto_fix and text_validation['suggested_font_size']: + try: + # Apply suggested font size + suggested_size = max(min_font_size, + min(text_validation['suggested_font_size'], max_font_size)) + + # Apply font size to all runs in the text frame + for paragraph in shape.text_frame.paragraphs: + for run in paragraph.runs: + if hasattr(run, 'font'): + run.font.size = suggested_size * 12700 # Convert to EMU + + fix = f"{shape_name}: Adjusted font size to {suggested_size}pt" + result['fixes_applied'].append(fix) + result['text_shapes_optimized'] += 1 + + except Exception as e: + warning = f"{shape_name}: Could not auto-fix font size: {str(e)}" + result['warnings'].append(warning) + + # Check for other potential issues + if len(shape.text_frame.text) > 500: # Very long text + result['warnings'].append(f"{shape_name}: Contains very long text (>500 chars)") + + # Check for empty paragraphs + empty_paragraphs = sum(1 for p in shape.text_frame.paragraphs if not p.text.strip()) + if empty_paragraphs > 2: + result['warnings'].append(f"{shape_name}: Contains {empty_paragraphs} empty paragraphs") + + # Check slide-level issues + if len(slide.shapes) > 20: + result['warnings'].append("Slide contains many shapes (>20), may affect performance") + + # Summary + if result['validation_passed']: + result['summary'] = "Slide validation passed successfully" + else: + result['summary'] = f"Found {len(result['issues_found'])} issues" + if auto_fix: + result['summary'] += f", applied {len(result['fixes_applied'])} fixes" + + return result + + except Exception as e: + result['validation_passed'] = False + result['error'] = str(e) + return result + + +def validate_slide_layout(slide) -> Dict: + """ + Validate slide layout for common issues. + + Args: + slide: The slide object + + Returns: + Dictionary with layout validation results + """ + result = { + 'layout_valid': True, + 'issues': [], + 'suggestions': [], + 'shape_count': len(slide.shapes), + 'overlapping_shapes': [] + } + + try: + shapes = list(slide.shapes) + + # Check for overlapping shapes + for i, shape1 in enumerate(shapes): + for j, shape2 in enumerate(shapes[i+1:], i+1): + if shapes_overlap(shape1, shape2): + result['overlapping_shapes'].append({ + 'shape1_index': i, + 'shape2_index': j, + 'shape1_name': getattr(shape1, 'name', f'Shape {i}'), + 'shape2_name': getattr(shape2, 'name', f'Shape {j}') + }) + + if result['overlapping_shapes']: + result['layout_valid'] = False + result['issues'].append(f"Found {len(result['overlapping_shapes'])} overlapping shapes") + result['suggestions'].append("Consider repositioning overlapping shapes") + + # Check for shapes outside slide boundaries + slide_width = 10 * 914400 # Standard slide width in EMU + slide_height = 7.5 * 914400 # Standard slide height in EMU + + shapes_outside = [] + for i, shape in enumerate(shapes): + if (shape.left < 0 or shape.top < 0 or + shape.left + shape.width > slide_width or + shape.top + shape.height > slide_height): + shapes_outside.append(i) + + if shapes_outside: + result['layout_valid'] = False + result['issues'].append(f"Found {len(shapes_outside)} shapes outside slide boundaries") + result['suggestions'].append("Reposition shapes to fit within slide boundaries") + + # Check shape spacing + if len(shapes) > 1: + min_spacing = check_minimum_spacing(shapes) + if min_spacing < 0.1 * 914400: # Less than 0.1 inch spacing + result['suggestions'].append("Consider increasing spacing between shapes") + + return result + + except Exception as e: + result['layout_valid'] = False + result['error'] = str(e) + return result + + +def shapes_overlap(shape1, shape2) -> bool: + """ + Check if two shapes overlap. + + Args: + shape1: First shape + shape2: Second shape + + Returns: + True if shapes overlap, False otherwise + """ + try: + # Get boundaries + left1, top1 = shape1.left, shape1.top + right1, bottom1 = left1 + shape1.width, top1 + shape1.height + + left2, top2 = shape2.left, shape2.top + right2, bottom2 = left2 + shape2.width, top2 + shape2.height + + # Check for overlap + return not (right1 <= left2 or right2 <= left1 or bottom1 <= top2 or bottom2 <= top1) + except: + return False + + +def check_minimum_spacing(shapes: List) -> float: + """ + Check minimum spacing between shapes. + + Args: + shapes: List of shapes + + Returns: + Minimum spacing found between shapes (in EMU) + """ + min_spacing = float('inf') + + try: + for i, shape1 in enumerate(shapes): + for shape2 in shapes[i+1:]: + # Calculate distance between shape edges + distance = calculate_shape_distance(shape1, shape2) + min_spacing = min(min_spacing, distance) + + return min_spacing if min_spacing != float('inf') else 0 + except: + return 0 + + +def calculate_shape_distance(shape1, shape2) -> float: + """ + Calculate distance between two shapes. + + Args: + shape1: First shape + shape2: Second shape + + Returns: + Distance between shape edges (in EMU) + """ + try: + # Get centers + center1_x = shape1.left + shape1.width / 2 + center1_y = shape1.top + shape1.height / 2 + + center2_x = shape2.left + shape2.width / 2 + center2_y = shape2.top + shape2.height / 2 + + # Calculate center-to-center distance + dx = abs(center2_x - center1_x) + dy = abs(center2_y - center1_y) + + # Subtract half-widths and half-heights to get edge distance + edge_distance_x = max(0, dx - (shape1.width + shape2.width) / 2) + edge_distance_y = max(0, dy - (shape1.height + shape2.height) / 2) + + # Return minimum edge distance + return min(edge_distance_x, edge_distance_y) + except: + return 0 \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/uv.lock new file mode 100644 index 00000000..23e6c2f6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/uv.lock @@ -0,0 +1,1137 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +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/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" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, + { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, + { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +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/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]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +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/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/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "office-powerpoint-mcp-server" +version = "2.0.7" +source = { editable = "." } +dependencies = [ + { name = "fonttools" }, + { name = "mcp", extra = ["cli"] }, + { name = "pillow" }, + { name = "python-pptx" }, +] + +[package.metadata] +requires-dist = [ + { name = "fonttools", specifier = ">=4.0.0" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.8.0" }, + { name = "pillow", specifier = ">=8.0.0" }, + { name = "python-pptx", specifier = ">=0.6.21" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "python-pptx" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "pillow" }, + { name = "typing-extensions" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, +] + +[[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" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +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/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.github/workflows/publish.yml new file mode 100644 index 00000000..3f4a74ef --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.gitignore new file mode 100644 index 00000000..f4fde46e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.gitignore @@ -0,0 +1,16 @@ +# Project files +.idea +.DS_Store + +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +.env.example +.idea diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/Dockerfile new file mode 100644 index 00000000..666236c1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/Dockerfile @@ -0,0 +1,22 @@ +# Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config +# syntax=docker/dockerfile:1 + +# Use official Python runtime +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install build dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY . /app + +# Install Python dependencies +RUN pip install --no-cache-dir . + +# Default command +ENTRYPOINT ["word_mcp_server"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/LICENSE new file mode 100644 index 00000000..31323d1d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 GongRzhe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/README.md new file mode 100644 index 00000000..20b5d25f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/README.md @@ -0,0 +1,394 @@ +# Office-Word-MCP-Server + +[![smithery badge](https://smithery.ai/badge/@GongRzhe/Office-Word-MCP-Server)](https://smithery.ai/server/@GongRzhe/Office-Word-MCP-Server) + +A Model Context Protocol (MCP) server for creating, reading, and manipulating Microsoft Word documents. This server enables AI assistants to work with Word documents through a standardized interface, providing rich document editing capabilities. + + + Office Word Server MCP server + + +![](https://badge.mcpx.dev?type=server "MCP Server") + +## Overview + +Office-Word-MCP-Server implements the [Model Context Protocol](https://modelcontextprotocol.io/) to expose Word document operations as tools and resources. It serves as a bridge between AI assistants and Microsoft Word documents, allowing for document creation, content addition, formatting, and analysis. + +The server features a modular architecture that separates concerns into core functionality, tools, and utilities, making it highly maintainable and extensible for future enhancements. + +### Example + +#### Pormpt + +![image](https://github.com/user-attachments/assets/f49b0bcc-88b2-4509-bf50-995b9a40038c) + +#### Output + +![image](https://github.com/user-attachments/assets/ff64385d-3822-4160-8cdf-f8a484ccc01a) + +## Features + +### Document Management + +- Create new Word documents with metadata +- Extract text and analyze document structure +- View document properties and statistics +- List available documents in a directory +- Create copies of existing documents +- Merge multiple documents into a single document +- Convert Word documents to PDF format + +### Content Creation + +- Add headings with different levels and direct formatting (font, size, bold, italic, borders) +- Insert paragraphs with optional styling and direct formatting (font, size, bold, italic, color) +- Create tables with custom data +- Add images with proportional scaling +- Insert page breaks +- Insert bulleted and numbered lists with proper XML formatting +- Add footnotes and endnotes to documents +- Convert footnotes to endnotes +- Customize footnote and endnote styling +- Create professional table layouts for technical documentation +- Design callout boxes and formatted content for instructional materials +- Build structured data tables for business reports with consistent styling +- Insert content relative to existing text or paragraph indices + +### Rich Text Formatting + +- Format specific text sections (bold, italic, underline) +- Change text color and font properties +- Apply custom styles to text elements +- Search and replace text throughout documents +- Individual cell text formatting within tables +- Multiple formatting combinations for enhanced visual appeal +- Font customization with family and size control +- Direct formatting during content creation (paragraphs and headings) +- Reduce function calls by combining content creation with formatting +- Add section header borders for visual separation + +### Table Formatting + +- Format tables with borders and styles +- Create header rows with distinct formatting +- Apply cell shading and custom borders +- Structure tables for better readability +- Individual cell background shading with color support +- Alternating row colors for improved readability +- Enhanced header row highlighting with custom colors +- Cell text formatting with bold, italic, underline, color, font size, and font family +- Comprehensive color support with named colors and hex color codes +- Cell padding management with independent control of all sides +- Cell alignment (horizontal and vertical positioning) +- Cell merging (horizontal, vertical, and rectangular areas) +- Column width management with multiple units (points, percentage, auto-fit) +- Auto-fit capabilities for dynamic column sizing +- Professional callout table support with icon cells and styled content + +### Advanced Document Manipulation + +- Delete paragraphs +- Insert content relative to specific text or paragraph indices +- Insert bulleted and numbered lists with proper XML numbering structure +- Insert headers and paragraphs before or after target locations +- Create custom document styles +- Apply consistent formatting throughout documents +- Format specific ranges of text with detailed control +- Flexible padding units with support for points and percentage-based measurements +- Clear, readable table presentation with proper alignment and spacing + +### Document Protection + +- Add password protection to documents +- Implement restricted editing with editable sections +- Add digital signatures to documents +- Verify document authenticity and integrity + +### Comment Extraction + +- Extract all comments from a document +- Filter comments by author +- Get comments for specific paragraphs +- Access comment metadata (author, date, text) + +## Installation + +### Installing via Smithery + +To install Office Word Document Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@GongRzhe/Office-Word-MCP-Server): + +```bash +npx -y @smithery/cli install @GongRzhe/Office-Word-MCP-Server --client claude +``` + +### Prerequisites + +- Python 3.8 or higher +- pip package manager + +### Basic Installation + +```bash +# Clone the repository +git clone https://github.com/GongRzhe/Office-Word-MCP-Server.git +cd Office-Word-MCP-Server + +# Install dependencies +pip install -r requirements.txt +``` + +### Using the Setup Script + +Alternatively, you can use the provided setup script which handles: + +- Checking prerequisites +- Setting up a virtual environment +- Installing dependencies +- Generating MCP configuration + +```bash +python setup_mcp.py +``` + +## Usage with Claude for Desktop + +### Configuration + +#### Method 1: After Local Installation + +1. After installation, add the server to your Claude for Desktop configuration file: + +```json +{ + "mcpServers": { + "word-document-server": { + "command": "python", + "args": ["/path/to/word_mcp_server.py"] + } + } +} +``` + +#### Method 2: Without Installation (Using uvx) + +1. You can also configure Claude for Desktop to use the server without local installation by using the uvx package manager: + +```json +{ + "mcpServers": { + "word-document-server": { + "command": "uvx", + "args": ["--from", "office-word-mcp-server", "word_mcp_server"] + } + } +} +``` + +2. Configuration file locations: + + - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` + - Windows: `%APPDATA%\Claude\claude_desktop_config.json` + +3. Restart Claude for Desktop to load the configuration. + +### Example Operations + +Once configured, you can ask Claude to perform operations like: + +- "Create a new document called 'report.docx' with a title page" +- "Add a heading and three paragraphs to my document" +- "Add my name in Helvetica 36pt bold at the top of the document" +- "Add a section heading 'Summary' in Helvetica 14pt bold with a bottom border" +- "Add a paragraph in Times New Roman 14pt with italic blue text" +- "Insert a bulleted list after the paragraph containing 'Introduction'" +- "Insert a numbered list with items: 'First step', 'Second step', 'Third step'" +- "Add bullet points after the 'Summary' heading" +- "Insert a 4x4 table with sales data" +- "Format the word 'important' in paragraph 2 to be bold and red" +- "Search and replace all instances of 'old term' with 'new term'" +- "Create a custom style for section headings" +- "Apply formatting to the table in my document" +- "Extract all comments from my document" +- "Show me all comments by John Doe" +- "Get comments for paragraph 3" +- "Make the text in table cell (1,2) bold and blue with 14pt font" +- "Add 10 points of padding to all sides of the header cells" +- "Create a callout table with a blue checkmark icon and white text" +- "Set the first column width to 50 points and auto-fit the remaining columns" +- "Apply alternating row colors to make the table more readable" + + +## API Reference + +### Document Creation and Properties + +```python +create_document(filename, title=None, author=None) +get_document_info(filename) +get_document_text(filename) +get_document_outline(filename) +list_available_documents(directory=".") +copy_document(source_filename, destination_filename=None) +convert_to_pdf(filename, output_filename=None) +``` + +### Content Addition + +```python +add_heading(filename, text, level=1, font_name=None, font_size=None, + bold=None, italic=None, border_bottom=False) +add_paragraph(filename, text, style=None, font_name=None, font_size=None, + bold=None, italic=None, color=None) +add_table(filename, rows, cols, data=None) +add_picture(filename, image_path, width=None) +add_page_break(filename) +``` + +### Advanced Content Manipulation + +```python +# Insert content relative to existing text or paragraph index +insert_header_near_text(filename, target_text=None, header_title=None, + position='after', header_style='Heading 1', + target_paragraph_index=None) + +insert_line_or_paragraph_near_text(filename, target_text=None, line_text=None, + position='after', line_style=None, + target_paragraph_index=None) + +# Insert bulleted or numbered lists with proper XML formatting +insert_numbered_list_near_text(filename, target_text=None, list_items=None, + position='after', target_paragraph_index=None, + bullet_type='bullet') +# bullet_type options: +# 'bullet' - Creates bulleted list with bullets (•) +# 'number' - Creates numbered list (1, 2, 3, ...) +``` + +### Content Extraction + +```python +get_document_text(filename) +get_paragraph_text_from_document(filename, paragraph_index) +find_text_in_document(filename, text_to_find, match_case=True, whole_word=False) +``` + +### Text Formatting + +```python +format_text(filename, paragraph_index, start_pos, end_pos, bold=None, + italic=None, underline=None, color=None, font_size=None, font_name=None) +search_and_replace(filename, find_text, replace_text) +delete_paragraph(filename, paragraph_index) +create_custom_style(filename, style_name, bold=None, italic=None, + font_size=None, font_name=None, color=None, base_style=None) +``` + +### Table Formatting + +```python +format_table(filename, table_index, has_header_row=None, + border_style=None, shading=None) +set_table_cell_shading(filename, table_index, row_index, col_index, + fill_color, pattern="clear") +apply_table_alternating_rows(filename, table_index, + color1="FFFFFF", color2="F2F2F2") +highlight_table_header(filename, table_index, + header_color="4472C4", text_color="FFFFFF") + +# Cell merging tools +merge_table_cells(filename, table_index, start_row, start_col, end_row, end_col) +merge_table_cells_horizontal(filename, table_index, row_index, start_col, end_col) +merge_table_cells_vertical(filename, table_index, col_index, start_row, end_row) + +# Cell alignment tools +set_table_cell_alignment(filename, table_index, row_index, col_index, + horizontal="left", vertical="top") +set_table_alignment_all(filename, table_index, + horizontal="left", vertical="top") + +# Cell text formatting tools +format_table_cell_text(filename, table_index, row_index, col_index, + text_content=None, bold=None, italic=None, underline=None, + color=None, font_size=None, font_name=None) + +# Cell padding tools +set_table_cell_padding(filename, table_index, row_index, col_index, + top=None, bottom=None, left=None, right=None, unit="points") + +# Column width management +set_table_column_width(filename, table_index, col_index, width, width_type="points") +set_table_column_widths(filename, table_index, widths, width_type="points") +set_table_width(filename, table_index, width, width_type="points") +auto_fit_table_columns(filename, table_index) +``` + +### Comment Extraction + +```python +get_all_comments(filename) +get_comments_by_author(filename, author) +get_comments_for_paragraph(filename, paragraph_index) +``` + +## Troubleshooting + +### Common Issues + +1. **Missing Styles** + + - Some documents may lack required styles for heading and table operations + - The server will attempt to create missing styles or use direct formatting + - For best results, use templates with standard Word styles + +2. **Permission Issues** + + - Ensure the server has permission to read/write to the document paths + - Use the `copy_document` function to create editable copies of locked documents + - Check file ownership and permissions if operations fail + +3. **Image Insertion Problems** + - Use absolute paths for image files + - Verify image format compatibility (JPEG, PNG recommended) + - Check image file size and permissions + +4. **Table Formatting Issues** + + - **Cell index errors**: Ensure row and column indices are within table bounds (0-based indexing) + - **Color format problems**: Use hex colors without '#' prefix (e.g., "FF0000" for red) or standard color names + - **Padding unit confusion**: Specify "points" or "percent" explicitly when setting cell padding + - **Column width conflicts**: Auto-fit may override manual column width settings + - **Text formatting persistence**: Apply cell text formatting after setting cell content for best results + +### Debugging + +Enable detailed logging by setting the environment variable: + +```bash +export MCP_DEBUG=1 # Linux/macOS +set MCP_DEBUG=1 # Windows +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- [Model Context Protocol](https://modelcontextprotocol.io/) for the protocol specification +- [python-docx](https://python-docx.readthedocs.io/) for Word document manipulation +- [FastMCP](https://github.com/modelcontextprotocol/python-sdk) for the Python MCP implementation + +--- + +_Note: This server interacts with document files on your system. Always verify that requested operations are appropriate before confirming them in Claude for Desktop or other MCP clients._ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/RENDER_DEPLOYMENT.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/RENDER_DEPLOYMENT.md new file mode 100644 index 00000000..70b12704 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/RENDER_DEPLOYMENT.md @@ -0,0 +1,59 @@ +# Render Deployment Guide + +This document explains how to deploy the Office Word MCP Server on Render. + +## Required Environment Variables + +Set the following environment variables in your Render service: + +### `MCP_TRANSPORT` +- **Value**: `sse` +- **Description**: Sets the transport type to Server-Sent Events (SSE) for HTTP communication +- **Required**: Yes (for Render deployment) + +### `MCP_HOST` +- **Value**: `0.0.0.0` +- **Description**: Binds the server to all network interfaces +- **Required**: No (defaults to 0.0.0.0) + +### `FASTMCP_LOG_LEVEL` +- **Value**: `INFO` +- **Description**: Sets the logging level for FastMCP +- **Required**: No (defaults to INFO) + +## How to Set Environment Variables + +1. Go to your Render dashboard: https://dashboard.render.com +2. Navigate to your service: `Office-Word-MCP-Server` +3. Click on "Environment" in the left sidebar +4. Add the environment variable: + - Key: `MCP_TRANSPORT` + - Value: `sse` +5. Click "Save Changes" + +## Deployment + +After setting the environment variables: +1. Render will automatically redeploy your service +2. The server will start with SSE transport on the port provided by Render +3. Access your server at: `https://office-word-mcp-server-bzlp.onrender.com/sse` + +## Health Check Endpoint + +The FastMCP server with SSE transport automatically provides a health check endpoint at: +- `https://your-service.onrender.com/health` + +## Troubleshooting + +### Server exits with status 1 +- **Cause**: Server is running in STDIO mode instead of SSE +- **Fix**: Ensure `MCP_TRANSPORT=sse` is set in environment variables + +### Port binding errors +- **Cause**: Server not using Render's PORT environment variable +- **Fix**: This has been fixed in the latest version of main.py + +### Cannot connect to server +- **Cause**: Health checks failing +- **Fix**: Ensure SSE transport is enabled and server is listening on 0.0.0.0 + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/__init__.py new file mode 100644 index 00000000..d9b86b65 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/__init__.py @@ -0,0 +1,4 @@ +"""Office Word MCP Server package entry point.""" +from word_document_server.main import run_server + +__all__ = ["run_server"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/mcp-config.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/mcp-config.json new file mode 100644 index 00000000..246a3ef2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/mcp-config.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "word-document-server": { + "command": "/Users/gongzhe/GitRepos/Office-Word-MCP-Server/.venv/bin/python", + "args": [ + "/Users/gongzhe/GitRepos/Office-Word-MCP-Server/word_mcp_server.py" + ], + "env": { + "PYTHONPATH": "/Users/gongzhe/GitRepos/Office-Word-MCP-Server", + "MCP_TRANSPORT": "stdio" + } + } + } +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/office_word_mcp_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/office_word_mcp_server/__init__.py new file mode 100644 index 00000000..d71a049f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/office_word_mcp_server/__init__.py @@ -0,0 +1,3 @@ +from word_document_server.main import run_server + +__all__ = ["run_server"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/pyproject.toml new file mode 100644 index 00000000..ca9ea6a0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "office-word-mcp-server" +version = "1.1.11" +description = "MCP server for manipulating Microsoft Word documents" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + {name = "GongRzhe", email = "gongrzhe@gmail.com"} +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +requires-python = ">=3.11" +dependencies = [ + "python-docx>=1.1.2", + "fastmcp>=2.8.1", + "msoffcrypto-tool>=5.4.2", + "docx2pdf>=0.1.8", + "pytest>=8.4.2", +] + +[project.urls] +"Homepage" = "https://github.com/GongRzhe/Office-Word-MCP-Server.git" +"Bug Tracker" = "https://github.com/GongRzhe/Office-Word-MCP-Server.git/issues" + +[tool.hatch.build.targets.wheel] +only-include = [ + "word_document_server", + "office_word_mcp_server", +] +sources = ["."] + +[project.scripts] +word_mcp_server = "word_document_server.main:run_server" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/requirements.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/requirements.txt new file mode 100644 index 00000000..7077dbb0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/requirements.txt @@ -0,0 +1,5 @@ +fastmcp +python-docx +msoffcrypto-tool +docx2pdf +python-dotenv \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/setup_mcp.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/setup_mcp.py new file mode 100644 index 00000000..f0caa5c4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/setup_mcp.py @@ -0,0 +1,524 @@ +# Import necessary Python standard libraries +import os +import json +import subprocess +import sys +import shutil +import platform + +def check_prerequisites(): + """ + Check if necessary prerequisites are installed + + Returns: + tuple: (python_ok, uv_installed, uvx_installed, word_server_installed) + """ + # Check Python version + python_version = sys.version_info + python_ok = python_version.major >= 3 and python_version.minor >= 8 + + # Check if uv/uvx is installed + uv_installed = shutil.which("uv") is not None + uvx_installed = shutil.which("uvx") is not None + + # Check if word-document-server is already installed via pip + try: + result = subprocess.run( + [sys.executable, "-m", "pip", "show", "word-document-server"], + capture_output=True, + text=True, + check=False + ) + word_server_installed = result.returncode == 0 + except Exception: + word_server_installed = False + + return (python_ok, uv_installed, uvx_installed, word_server_installed) + +def get_transport_choice(): + """ + Ask user to choose transport type + + Returns: + dict: Transport configuration + """ + print("\nTransport Configuration:") + print("1. STDIO (default, local execution)") + print("2. Streamable HTTP (modern, recommended for web deployment)") + print("3. SSE (Server-Sent Events, for compatibility)") + + choice = input("\nSelect transport type (1-3, default: 1): ").strip() + + if choice == "2": + host = input("Host (default: 127.0.0.1): ").strip() or "127.0.0.1" + port = input("Port (default: 8000): ").strip() or "8000" + path = input("Path (default: /mcp): ").strip() or "/mcp" + + return { + "transport": "streamable-http", + "host": host, + "port": port, + "path": path + } + elif choice == "3": + host = input("Host (default: 127.0.0.1): ").strip() or "127.0.0.1" + port = input("Port (default: 8000): ").strip() or "8000" + sse_path = input("SSE Path (default: /sse): ").strip() or "/sse" + + return { + "transport": "sse", + "host": host, + "port": port, + "sse_path": sse_path + } + else: + # Default to stdio + return { + "transport": "stdio" + } + +def setup_venv(): + """ + Function to set up Python virtual environment + + Features: + - Checks if Python version meets requirements (3.8+) + - Creates Python virtual environment (if it doesn't exist) + - Installs required dependencies in the newly created virtual environment + + No parameters required + + Returns: Path to Python interpreter in the virtual environment + """ + # Check Python version + python_version = sys.version_info + if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 8): + print("Error: Python 3.8 or higher is required.") + sys.exit(1) + + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + # Set virtual environment directory path + venv_path = os.path.join(base_path, '.venv') + + # Determine pip and python executable paths based on operating system + is_windows = platform.system() == "Windows" + if is_windows: + pip_path = os.path.join(venv_path, 'Scripts', 'pip.exe') + python_path = os.path.join(venv_path, 'Scripts', 'python.exe') + else: + pip_path = os.path.join(venv_path, 'bin', 'pip') + python_path = os.path.join(venv_path, 'bin', 'python') + + # Check if virtual environment already exists and is valid + venv_exists = os.path.exists(venv_path) + pip_exists = os.path.exists(pip_path) + + if not venv_exists or not pip_exists: + print("Creating new virtual environment...") + # Remove existing venv if it's invalid + if venv_exists and not pip_exists: + print("Existing virtual environment is incomplete, recreating it...") + try: + shutil.rmtree(venv_path) + except Exception as e: + print(f"Warning: Could not remove existing virtual environment: {e}") + print("Please delete the .venv directory manually and try again.") + sys.exit(1) + + # Create virtual environment + try: + subprocess.run([sys.executable, '-m', 'venv', venv_path], check=True) + print("Virtual environment created successfully!") + except subprocess.CalledProcessError as e: + print(f"Error creating virtual environment: {e}") + sys.exit(1) + else: + print("Valid virtual environment already exists.") + + # Double-check that pip exists after creating venv + if not os.path.exists(pip_path): + print(f"Error: pip executable not found at {pip_path}") + print("Try creating the virtual environment manually with: python -m venv .venv") + sys.exit(1) + + # Install or update dependencies + print("\nInstalling requirements...") + try: + # Install FastMCP package (standalone library) + subprocess.run([pip_path, 'install', 'fastmcp'], check=True) + # Install python-docx package + subprocess.run([pip_path, 'install', 'python-docx'], check=True) + + # Also install dependencies from requirements.txt if it exists + requirements_path = os.path.join(base_path, 'requirements.txt') + if os.path.exists(requirements_path): + subprocess.run([pip_path, 'install', '-r', requirements_path], check=True) + + print("Requirements installed successfully!") + except subprocess.CalledProcessError as e: + print(f"Error installing requirements: {e}") + sys.exit(1) + except FileNotFoundError: + print(f"Error: Could not execute {pip_path}") + print("Try activating the virtual environment manually and installing requirements:") + if is_windows: + print(f".venv\\Scripts\\activate") + else: + print("source .venv/bin/activate") + print("pip install mcp[cli] python-docx") + sys.exit(1) + + return python_path + +def generate_mcp_config_local(python_path, transport_config): + """ + Generate MCP configuration for locally installed word-document-server + + Parameters: + - python_path: Path to Python interpreter in the virtual environment + - transport_config: Transport configuration dictionary + + Returns: Path to the generated config file + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Path to Word Document Server script + server_script_path = os.path.join(base_path, 'word_mcp_server.py') + + # Build environment variables + env = { + "PYTHONPATH": base_path, + "MCP_TRANSPORT": transport_config["transport"] + } + + # Add transport-specific environment variables + if transport_config["transport"] == "streamable-http": + env.update({ + "MCP_HOST": transport_config["host"], + "MCP_PORT": transport_config["port"], + "MCP_PATH": transport_config["path"] + }) + elif transport_config["transport"] == "sse": + env.update({ + "MCP_HOST": transport_config["host"], + "MCP_PORT": transport_config["port"], + "MCP_SSE_PATH": transport_config["sse_path"] + }) + # For stdio transport, no additional environment variables needed + + # Create MCP configuration dictionary + config = { + "mcpServers": { + "word-document-server": { + "command": python_path, + "args": [server_script_path], + "env": env + } + } + } + + # Save configuration to JSON file + config_path = os.path.join(base_path, 'mcp-config.json') + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) + + return config_path + +def generate_mcp_config_uvx(transport_config): + """ + Generate MCP configuration for PyPI-installed word-document-server using UVX + + Parameters: + - transport_config: Transport configuration dictionary + + Returns: Path to the generated config file + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Build environment variables + env = { + "MCP_TRANSPORT": transport_config["transport"] + } + + # Add transport-specific environment variables + if transport_config["transport"] == "streamable-http": + env.update({ + "MCP_HOST": transport_config["host"], + "MCP_PORT": transport_config["port"], + "MCP_PATH": transport_config["path"] + }) + elif transport_config["transport"] == "sse": + env.update({ + "MCP_HOST": transport_config["host"], + "MCP_PORT": transport_config["port"], + "MCP_SSE_PATH": transport_config["sse_path"] + }) + # For stdio transport, no additional environment variables needed + + # Create MCP configuration dictionary + config = { + "mcpServers": { + "word-document-server": { + "command": "uvx", + "args": ["--from", "word-mcp-server", "word_mcp_server"], + "env": env + } + } + } + + # Save configuration to JSON file + config_path = os.path.join(base_path, 'mcp-config.json') + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) + + return config_path + +def generate_mcp_config_module(transport_config): + """ + Generate MCP configuration for PyPI-installed word-document-server using Python module + + Parameters: + - transport_config: Transport configuration dictionary + + Returns: Path to the generated config file + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Build environment variables + env = { + "MCP_TRANSPORT": transport_config["transport"] + } + + # Add transport-specific environment variables + if transport_config["transport"] == "streamable-http": + env.update({ + "MCP_HOST": transport_config["host"], + "MCP_PORT": transport_config["port"], + "MCP_PATH": transport_config["path"] + }) + elif transport_config["transport"] == "sse": + env.update({ + "MCP_HOST": transport_config["host"], + "MCP_PORT": transport_config["port"], + "MCP_SSE_PATH": transport_config["sse_path"] + }) + + + # Create MCP configuration dictionary + config = { + "mcpServers": { + "word-document-server": { + "command": sys.executable, + "args": ["-m", "word_document_server"], + "env": env + } + } + } + + # Save configuration to JSON file + config_path = os.path.join(base_path, 'mcp-config.json') + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) + + return config_path + +def install_from_pypi(): + """ + Install word-document-server from PyPI + + Returns: True if successful, False otherwise + """ + print("\nInstalling word-document-server from PyPI...") + try: + subprocess.run([sys.executable, "-m", "pip", "install", "word-mcp-server"], check=True) + print("word-mcp-server successfully installed from PyPI!") + return True + except subprocess.CalledProcessError: + print("Failed to install word-mcp-server from PyPI.") + return False + +def print_config_instructions(config_path, transport_config): + """ + Print instructions for using the generated config + + Parameters: + - config_path: Path to the generated config file + - transport_config: Transport configuration dictionary + """ + print(f"\nMCP configuration has been written to: {config_path}") + + with open(config_path, 'r') as f: + config = json.load(f) + + print("\nMCP configuration for Claude Desktop:") + print(json.dumps(config, indent=2)) + + # Print transport-specific instructions + if transport_config["transport"] == "streamable-http": + print(f"\n📡 Streamable HTTP Transport Configuration:") + print(f" Server will be accessible at: http://{transport_config['host']}:{transport_config['port']}{transport_config['path']}") + print(f" \n To test the server manually:") + print(f" curl -X POST http://{transport_config['host']}:{transport_config['port']}{transport_config['path']}") + + elif transport_config["transport"] == "sse": + print(f"\n📡 SSE Transport Configuration:") + print(f" Server will be accessible at: http://{transport_config['host']}:{transport_config['port']}{transport_config['sse_path']}") + print(f" \n To test the server manually:") + print(f" curl http://{transport_config['host']}:{transport_config['port']}{transport_config['sse_path']}") + + else: # stdio + print(f"\n💻 STDIO Transport Configuration:") + print(f" Server runs locally with standard input/output") + + # Provide instructions for adding configuration to Claude Desktop configuration file + if platform.system() == "Windows": + claude_config_path = os.path.expandvars("%APPDATA%\\Claude\\claude_desktop_config.json") + else: # macOS + claude_config_path = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json") + + print(f"\nTo use with Claude Desktop, merge this configuration into: {claude_config_path}") + +def create_package_structure(): + """ + Create necessary package structure and environment files + """ + # Get absolute path of the directory containing the current script + base_path = os.path.abspath(os.path.dirname(__file__)) + + # Create __init__.py file + init_path = os.path.join(base_path, '__init__.py') + if not os.path.exists(init_path): + with open(init_path, 'w') as f: + f.write('# Word Document MCP Server') + print(f"Created __init__.py at: {init_path}") + + # Create requirements.txt file + requirements_path = os.path.join(base_path, 'requirements.txt') + if not os.path.exists(requirements_path): + with open(requirements_path, 'w') as f: + f.write('fastmcp\npython-docx\nmsoffcrypto-tool\ndocx2pdf\nhttpx\ncryptography\n') + print(f"Created requirements.txt at: {requirements_path}") + + # Create .env.example file + env_example_path = os.path.join(base_path, '.env.example') + if not os.path.exists(env_example_path): + with open(env_example_path, 'w') as f: + f.write("""# Transport Configuration +# Valid options: stdio, streamable-http, sse +MCP_TRANSPORT=stdio + +# HTTP/SSE Configuration (when not using stdio) +MCP_HOST=127.0.0.1 +MCP_PORT=8000 + +# Streamable HTTP specific +MCP_PATH=/mcp + +# SSE specific +MCP_SSE_PATH=/sse + +""") + print(f"Created .env.example at: {env_example_path}") + +# Main execution entry point +if __name__ == '__main__': + # Check prerequisites + python_ok, uv_installed, uvx_installed, word_server_installed = check_prerequisites() + + if not python_ok: + print("Error: Python 3.8 or higher is required.") + sys.exit(1) + + print("Word Document MCP Server Setup (Multi-Transport)") + print("===============================================\n") + + # Create necessary files + create_package_structure() + + # Get transport configuration + transport_config = get_transport_choice() + + # If word-document-server is already installed, offer config options + if word_server_installed: + print("word-document-server is already installed via pip.") + + if uvx_installed: + print("\nOptions:") + print("1. Generate MCP config for UVX (recommended)") + print("2. Generate MCP config for Python module") + print("3. Set up local development environment") + + choice = input("\nEnter your choice (1-3): ") + + if choice == "1": + config_path = generate_mcp_config_uvx(transport_config) + print_config_instructions(config_path, transport_config) + elif choice == "2": + config_path = generate_mcp_config_module(transport_config) + print_config_instructions(config_path, transport_config) + elif choice == "3": + python_path = setup_venv() + config_path = generate_mcp_config_local(python_path, transport_config) + print_config_instructions(config_path, transport_config) + else: + print("Invalid choice. Exiting.") + sys.exit(1) + else: + print("\nOptions:") + print("1. Generate MCP config for Python module") + print("2. Set up local development environment") + + choice = input("\nEnter your choice (1-2): ") + + if choice == "1": + config_path = generate_mcp_config_module(transport_config) + print_config_instructions(config_path, transport_config) + elif choice == "2": + python_path = setup_venv() + config_path = generate_mcp_config_local(python_path, transport_config) + print_config_instructions(config_path, transport_config) + else: + print("Invalid choice. Exiting.") + sys.exit(1) + + # If word-document-server is not installed, offer installation options + else: + print("word-document-server is not installed.") + + print("\nOptions:") + print("1. Install from PyPI (recommended)") + print("2. Set up local development environment") + + choice = input("\nEnter your choice (1-2): ") + + if choice == "1": + if install_from_pypi(): + if uvx_installed: + print("\nNow generating MCP config for UVX...") + config_path = generate_mcp_config_uvx(transport_config) + else: + print("\nUVX not found. Generating MCP config for Python module...") + config_path = generate_mcp_config_module(transport_config) + print_config_instructions(config_path, transport_config) + elif choice == "2": + python_path = setup_venv() + config_path = generate_mcp_config_local(python_path, transport_config) + print_config_instructions(config_path, transport_config) + else: + print("Invalid choice. Exiting.") + sys.exit(1) + + print("\nSetup complete! You can now use the Word Document MCP server with compatible clients like Claude Desktop.") + print("\nTransport Summary:") + print(f" - Transport: {transport_config['transport']}") + if transport_config['transport'] != 'stdio': + print(f" - Host: {transport_config.get('host', 'N/A')}") + print(f" - Port: {transport_config.get('port', 'N/A')}") + if transport_config['transport'] == 'streamable-http': + print(f" - Path: {transport_config.get('path', 'N/A')}") + elif transport_config['transport'] == 'sse': + print(f" - SSE Path: {transport_config.get('sse_path', 'N/A')}") \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/smithery.yaml new file mode 100644 index 00000000..49719515 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/smithery.yaml @@ -0,0 +1,13 @@ +# Smithery configuration file: https://smithery.ai/docs/build/project-config + +startCommand: + type: stdio + configSchema: + # JSON Schema defining the configuration options for the MCP. + type: object + description: No configuration options required + commandFunction: + # A JS function that produces the CLI command based on the given config to start the MCP on stdio. + |- + (config) => ({command:'word_mcp_server', args:[]}) + exampleConfig: {} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_doc.docx b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_doc.docx new file mode 100644 index 00000000..4bea9d7b Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_doc.docx differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_formatting.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_formatting.py new file mode 100644 index 00000000..f29f932a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_formatting.py @@ -0,0 +1,108 @@ +""" +Test script for add_paragraph and add_heading formatting parameters. +""" +import asyncio +from docx import Document +from word_document_server.tools.content_tools import add_paragraph, add_heading +from word_document_server.tools.document_tools import create_document + + +async def test_formatting(): + """Test the new formatting parameters.""" + test_doc = 'test_formatting.docx' + + # Create test document + print("Creating test document...") + await create_document(test_doc, title="Formatting Test", author="Test Script") + + # Test 1: Name with large font + print("Test 1: Adding name with large Helvetica 36pt bold...") + result = await add_paragraph( + test_doc, + "JAMES MEHORTER", + font_name="Helvetica", + font_size=36, + bold=True + ) + print(f" Result: {result}") + + # Test 2: Title line + print("Test 2: Adding title with Helvetica 14pt...") + result = await add_paragraph( + test_doc, + "Principal Software Engineer | Technical Team Lead", + font_name="Helvetica", + font_size=14 + ) + print(f" Result: {result}") + + # Test 3: Section header with border + print("Test 3: Adding section header with border...") + result = await add_heading( + test_doc, + "PROFESSIONAL SUMMARY", + level=2, + font_name="Helvetica", + font_size=14, + bold=True, + border_bottom=True + ) + print(f" Result: {result}") + + # Test 4: Body text in Times New Roman + print("Test 4: Adding body text in Times New Roman 14pt...") + result = await add_paragraph( + test_doc, + "This is body text that should be in Times New Roman at 14pt. " + "It demonstrates the ability to apply different fonts to different paragraphs.", + font_name="Times New Roman", + font_size=14 + ) + print(f" Result: {result}") + + # Test 5: Another section header + print("Test 5: Adding another section header with border...") + result = await add_heading( + test_doc, + "SKILLS", + level=2, + font_name="Helvetica", + font_size=14, + bold=True, + border_bottom=True + ) + print(f" Result: {result}") + + # Test 6: Italic text with color + print("Test 6: Adding italic text with color...") + result = await add_paragraph( + test_doc, + "This text is italic and colored blue.", + font_name="Arial", + font_size=12, + italic=True, + color="0000FF" + ) + print(f" Result: {result}") + + print(f"\n✅ Test document created: {test_doc}") + + # Verify formatting + print("\nVerifying formatting...") + verify_doc = Document(test_doc) + for i, para in enumerate(verify_doc.paragraphs): + if para.runs: + run = para.runs[0] + text_preview = para.text[:50] + "..." if len(para.text) > 50 else para.text + print(f"\nParagraph {i}: {text_preview}") + print(f" Font: {run.font.name}") + print(f" Size: {run.font.size}") + print(f" Bold: {run.font.bold}") + print(f" Italic: {run.font.italic}") + + print("\n✅ All tests completed successfully!") + print(f"Open {test_doc} in Word to verify the formatting visually.") + + +if __name__ == "__main__": + asyncio.run(test_formatting()) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/tests/test_convert_to_pdf.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/tests/test_convert_to_pdf.py new file mode 100644 index 00000000..c692fc82 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/tests/test_convert_to_pdf.py @@ -0,0 +1,84 @@ +import asyncio +from pathlib import Path + +import pytest +from docx import Document + +# Target for testing: convert_to_pdf (async function) +from word_document_server.tools.extended_document_tools import convert_to_pdf + + +def _make_sample_docx(path: Path) -> None: + """Generates a simple .docx file in a temporary directory.""" + doc = Document() + doc.add_heading("Conversion Test Document", level=1) + doc.add_paragraph("This is a test paragraph for PDF conversion. Contains ASCII too.") + doc.add_paragraph("Second paragraph: Contains special characters and spaces to cover path/content edge cases.") + doc.save(path) + + +def test_convert_to_pdf_with_temp_docx(tmp_path: Path): + """ + End-to-end test: Create a temporary .docx -> call convert_to_pdf -> validate the PDF output. + + Notes: + - On Linux/macOS, it first tries LibreOffice (soffice/libreoffice), + and falls back to docx2pdf on failure (requires Microsoft Word). + - If these tools are missing or the command is unavailable, the test is skipped with a reason. + """ + # 1) Generate a docx file with spaces in its name in the temp directory + src_doc = tmp_path / "sample document with spaces.docx" + _make_sample_docx(src_doc) + + # 2) Define the output PDF path (also in the temp directory) + out_pdf = tmp_path / "converted output.pdf" + + # 3) Run the asynchronous function under test + result_msg = asyncio.run(convert_to_pdf(str(src_doc), output_filename=str(out_pdf))) + + # 4) Success condition: the return message contains success keywords, or the target PDF exists + success_keywords = ["successfully converted", "converted to PDF"] + success = any(k.lower() in result_msg.lower() for k in success_keywords) or out_pdf.exists() + + if not success: + # When LibreOffice or Microsoft Word is not installed, the tool returns a hint. + # In this case, skip the test instead of failing. + pytest.skip(f"PDF conversion tool unavailable or conversion failed: {result_msg}") + + # 5) Assert: The PDF file was generated and is not empty + # Some environments (especially docx2pdf) might ignore the exact output filename + # and just generate a PDF with the same name as the source in the output or source directory, + # so we check multiple possible locations. + candidates = [ + out_pdf, + # Common: A PDF with the same name as the source file in the output directory + out_pdf.parent / f"{src_doc.stem}.pdf", + # Fallback: A PDF in the same directory as the source file + src_doc.with_suffix(".pdf"), + ] + + # If none of the above paths exist, search for any newly generated PDF in the temp directory + found = None + for p in candidates: + if p.exists(): + found = p + break + if not found: + pdfs = sorted(tmp_path.glob("*.pdf"), key=lambda p: p.stat().st_mtime, reverse=True) + if pdfs: + found = pdfs[0] + + if not found: + # If the tool returns success but the output can't be found, + # treat it as an environment/tooling difference and skip instead of failing. + pytest.skip(f"Could not find the generated PDF. Function output: {result_msg}") + + assert found.exists(), f"Generated PDF not found: {found}, function output: {result_msg}" + assert found.stat().st_size > 0, f"The generated PDF file is empty: {found}" + + +if __name__ == "__main__": + # Allow running this file directly for quick verification: + # python tests/test_convert_to_pdf.py + import sys + sys.exit(pytest.main([__file__, "-q"])) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/uv.lock new file mode 100644 index 00000000..d87a4b65 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/uv.lock @@ -0,0 +1,1491 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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" } +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" }, +] + +[[package]] +name = "appscript" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/84/5c0aec149c6a002d46af17e3d2c5efbe5e8258ef7574cfc17cd1b26c726e/appscript-1.3.0.tar.gz", hash = "sha256:80943118bc97f9f78a8aa55f85565752ed4d82c7893427d7d9ebfdf401c12b2c", size = 295205, upload-time = "2024-10-13T12:34:00.57Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/64/db8dddd3c561fe5085e5b3a60419bfb560f07e1ca0dc1c7027cbaa5fb582/appscript-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:76a3507b27c78bf79af83a5f6fac49664b53d530d75632c023e53df1bd350caf", size = 99353, upload-time = "2024-10-13T12:33:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/40/ee/4e0dee488d3dd35aab03c2f6ecb6dc0161fad200077cca68afe041079d2b/appscript-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94ca097d672de5b8cfc82b4179b00cabd21588dbfd939347cf14a9e81955b2d5", size = 85401, upload-time = "2024-10-13T12:33:52.46Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e2/05fd221bea1d309211569130a1a8f0966eb56394e46df068a69df0f29d61/appscript-1.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c0b5c160908de728072d4a0ae57f286608c5d7692bfccbc6eadde868aac2742b", size = 99575, upload-time = "2024-10-13T12:33:53.629Z" }, + { url = "https://files.pythonhosted.org/packages/df/2f/3ee4190ce97b0b39df58184210d3baaa5fe59ae0972e63c2c85f122ca887/appscript-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2a287b81030c81017127d4fb1c24729623576c50d2ff41694476b9af3ce0a97", size = 85496, upload-time = "2024-10-13T12:33:55.108Z" }, + { url = "https://files.pythonhosted.org/packages/92/5a/3b642e3e904fb37d45e40bb07b4362979160bdecb0d37aa74f2506b1a47e/appscript-1.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:13094640e2694b888827d4e133f33dad1e08c9d7102b447c3cc8a73246fdab40", size = 99574, upload-time = "2024-10-13T12:33:56.317Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bc/d8558bec737e02a9c404fb3b985b8636c313bb65a176375d551cb839e876/appscript-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7b4760105810e9b1ecd5b40aba7617e0a047346fb94ee4370e9d37e4383b78d", size = 85503, upload-time = "2024-10-13T12:33:57.54Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/5c/3b882b82e9af737906539a2eafb62f96a229f1fa80255bede0c7b554cbc4/cachetools-7.0.3.tar.gz", hash = "sha256:8c246313b95849964e54a909c03b327a87ab0428b068fac10da7b105ca275ef6", size = 37187, upload-time = "2026-03-05T21:00:57.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4a/573185481c50a8841331f54ddae44e4a3469c46aa0b397731c53a004369a/cachetools-7.0.3-py3-none-any.whl", hash = "sha256:c128ffca156eef344c25fcd08a96a5952803786fa33097f5f2d49edf76f79d53", size = 13907, upload-time = "2026-03-05T21:00:56.486Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "44.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807, upload-time = "2025-03-02T00:01:37.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361, upload-time = "2025-03-02T00:00:06.528Z" }, + { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350, upload-time = "2025-03-02T00:00:09.537Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572, upload-time = "2025-03-02T00:00:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124, upload-time = "2025-03-02T00:00:14.518Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122, upload-time = "2025-03-02T00:00:17.212Z" }, + { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831, upload-time = "2025-03-02T00:00:19.696Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583, upload-time = "2025-03-02T00:00:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753, upload-time = "2025-03-02T00:00:25.038Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550, upload-time = "2025-03-02T00:00:26.929Z" }, + { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367, upload-time = "2025-03-02T00:00:28.735Z" }, + { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843, upload-time = "2025-03-02T00:00:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057, upload-time = "2025-03-02T00:00:33.393Z" }, + { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789, upload-time = "2025-03-02T00:00:36.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919, upload-time = "2025-03-02T00:00:38.581Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812, upload-time = "2025-03-02T00:00:42.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571, upload-time = "2025-03-02T00:00:46.026Z" }, + { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832, upload-time = "2025-03-02T00:00:48.647Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719, upload-time = "2025-03-02T00:00:51.397Z" }, + { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852, upload-time = "2025-03-02T00:00:53.317Z" }, + { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906, upload-time = "2025-03-02T00:00:56.49Z" }, + { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572, upload-time = "2025-03-02T00:00:59.995Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631, upload-time = "2025-03-02T00:01:01.623Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792, upload-time = "2025-03-02T00:01:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957, upload-time = "2025-03-02T00:01:06.987Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513, upload-time = "2025-03-02T00:01:22.911Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432, upload-time = "2025-03-02T00:01:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421, upload-time = "2025-03-02T00:01:26.335Z" }, + { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081, upload-time = "2025-03-02T00:01:28.938Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/a7/61825c9c46dd9d3d2a231c9792753fc3fe2822a90734a619b1a23ed0f05f/cyclopts-4.7.0.tar.gz", hash = "sha256:1d0fd440b8d21a55d14f830033eb1ac156933424df3e90afeea34cfb3ed73822", size = 163447, upload-time = "2026-03-05T02:57:49.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/08/a631a99df0e9f49c73ec682a9d1e05e5887cf79f04076792aacb4caac6b2/cyclopts-4.7.0-py3-none-any.whl", hash = "sha256:c659d930797a8470f2914a8f8f8be263b339cb6ffb6593b4a59fa9d84b8e0e38", size = 201270, upload-time = "2026-03-05T02:57:50.988Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "docx2pdf" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appscript", marker = "sys_platform == 'darwin'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5d/112531fff53cf60513e14fa1707755c874d47880ec4de7b2235302ad19a0/docx2pdf-0.1.8.tar.gz", hash = "sha256:6d2c20f9ad36eec75f4da017dc7a97622946954a6124ca0b11772875fa86fbed", size = 6483, upload-time = "2021-12-11T16:56:36.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/4f/1155781308281e67f80b829738a29e5354e03664c62311f753056afc873b/docx2pdf-0.1.8-py3-none-any.whl", hash = "sha256:00be1401fd486640314e993423a0a1cbdbc21142186f68549d962d505b2e8a12", size = 6741, upload-time = "2021-12-11T16:56:35.163Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastmcp" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "uncalled-for" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/70/862026c4589441f86ad3108f05bfb2f781c6b322ad60a982f40b303b47d7/fastmcp-3.1.0.tar.gz", hash = "sha256:e25264794c734b9977502a51466961eeecff92a0c2f3b49c40c070993628d6d0", size = 17347083, upload-time = "2026-03-03T02:43:11.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/07/516f5b20d88932e5a466c2216b628e5358a71b3a9f522215607c3281de05/fastmcp-3.1.0-py3-none-any.whl", hash = "sha256:b1f73b56fd3b0cb2bd9e2a144fc650d5cc31587ed129d996db7710e464ae8010", size = 633749, upload-time = "2026-03-03T02:43:09.06Z" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385, upload-time = "2025-04-11T14:42:46.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732, upload-time = "2025-04-11T14:42:44.896Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +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" } +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" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +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/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]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "lxml" +version = "5.4.0" +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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" }, + { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" }, + { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" }, + { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, + { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, + { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, + { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, + { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, + { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, + { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, + { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, + { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, + { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, + { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, + { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "msoffcrypto-tool" +version = "5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "olefile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/b7/0fd6573157e0ec60c0c470e732ab3322fba4d2834fd24e1088d670522a01/msoffcrypto_tool-5.4.2.tar.gz", hash = "sha256:44b545adba0407564a0cc3d6dde6ca36b7c0fdf352b85bca51618fa1d4817370", size = 41183, upload-time = "2024-08-08T15:50:28.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/54/7f6d3d9acad083dae8c22d9ab483b657359a1bf56fee1d7af88794677707/msoffcrypto_tool-5.4.2-py3-none-any.whl", hash = "sha256:274fe2181702d1e5a107ec1b68a4c9fea997a44972ae1cc9ae0cb4f6a50fef0e", size = 48713, upload-time = "2024-08-08T15:50:27.093Z" }, +] + +[[package]] +name = "office-word-mcp-server" +version = "1.1.11" +source = { editable = "." } +dependencies = [ + { name = "docx2pdf" }, + { name = "fastmcp" }, + { name = "msoffcrypto-tool" }, + { name = "pytest" }, + { name = "python-docx" }, +] + +[package.metadata] +requires-dist = [ + { name = "docx2pdf", specifier = ">=0.1.8" }, + { name = "fastmcp", specifier = ">=2.8.1" }, + { name = "msoffcrypto-tool", specifier = ">=5.4.2" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "python-docx", specifier = ">=1.1.2" }, +] + +[[package]] +name = "olefile" +version = "0.47" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +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" } +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" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +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" } +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" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-docx" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/e4/386c514c53684772885009c12b67a7edd526c15157778ac1b138bc75063e/python_docx-1.1.2.tar.gz", hash = "sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd", size = 5656581, upload-time = "2024-05-01T19:41:57.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/3d/330d9efbdb816d3f60bf2ad92f05e1708e4a1b9abe80461ac3444c83f749/python_docx-1.1.2-py3-none-any.whl", hash = "sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe", size = 244315, upload-time = "2024-05-01T19:41:47.006Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +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" } +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" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, + { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, + { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/35/7d8d94eb0474352d55f60f80ebc30f7e59441a29e18886a6425f0bccd0d3/sse_starlette-2.3.3.tar.gz", hash = "sha256:fdd47c254aad42907cfd5c5b83e2282be15be6c51197bf1a9b70b8e990522072", size = 17499, upload-time = "2025-04-23T19:28:25.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/20/52fdb5ebb158294b0adb5662235dd396fc7e47aa31c293978d8d8942095a/sse_starlette-2.3.3-py3-none-any.whl", hash = "sha256:8b0a0ced04a329ff7341b01007580dd8cf71331cc21c0ccea677d500618da1e0", size = 10235, upload-time = "2025-04-23T19:28:24.115Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +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" } +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" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uncalled-for" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/__init__.py new file mode 100644 index 00000000..85d71f39 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/__init__.py @@ -0,0 +1,15 @@ +""" +Word Document Server - MCP server for Microsoft Word document manipulation. + +This package provides tools for creating, reading, and manipulating Microsoft Word +documents through the Model Context Protocol (MCP). + +Features: +- Document creation and management +- Content addition (headings, paragraphs, tables, images) +- Text and table formatting +- Document protection (password, restricted editing, signatures) +- Footnote and endnote management +""" + +__version__ = "1.0.0" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/__init__.py new file mode 100644 index 00000000..a0aa41db --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/__init__.py @@ -0,0 +1,10 @@ +""" +Core functionality for the Word Document Server. + +This package contains the core functionality modules used by the Word Document Server. +""" + +from word_document_server.core.styles import ensure_heading_style, ensure_table_style, create_style +from word_document_server.core.protection import add_protection_info, verify_document_protection, is_section_editable, create_signature_info, verify_signature +from word_document_server.core.footnotes import add_footnote, add_endnote, convert_footnotes_to_endnotes, find_footnote_references, get_format_symbols, customize_footnote_formatting +from word_document_server.core.tables import set_cell_border, apply_table_style, copy_table diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/comments.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/comments.py new file mode 100644 index 00000000..9695c8b6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/comments.py @@ -0,0 +1,210 @@ +""" +Core comment extraction functionality for Word documents. + +This module provides low-level functions to extract and process comments +from Word documents using the python-docx library. +""" +import datetime +from typing import Dict, List, Optional, Any +from docx import Document +from docx.document import Document as DocumentType +from docx.text.paragraph import Paragraph + + +def extract_all_comments(doc: DocumentType) -> List[Dict[str, Any]]: + """ + Extract all comments from a Word document. + + Args: + doc: The Document object to extract comments from + + Returns: + List of dictionaries containing comment information + """ + comments = [] + + # Access the document's comment part if it exists + try: + # Get the document part + document_part = doc.part + + # Find comments part through relationships + comments_part = None + for rel_id, rel in document_part.rels.items(): + if 'comments' in rel.reltype and 'comments' == rel.reltype.split('/')[-1]: + comments_part = rel.target_part + break + + if comments_part: + # Extract comments from the comments part using proper xpath syntax + comment_elements = comments_part.element.xpath('.//w:comment') + + for idx, comment_element in enumerate(comment_elements): + comment_data = extract_comment_data(comment_element, idx) + if comment_data: + comments.append(comment_data) + + # If no comments found, try alternative approach + if not comments: + # Fallback: scan paragraphs for comment references + comments = extract_comments_from_paragraphs(doc) + + except Exception as e: + # If direct access fails, try alternative approach + comments = extract_comments_from_paragraphs(doc) + + return comments + + +def extract_comments_from_paragraphs(doc: DocumentType) -> List[Dict[str, Any]]: + """ + Extract comments by scanning paragraphs for comment references. + + Args: + doc: The Document object + + Returns: + List of comment dictionaries + """ + comments = [] + comment_id = 1 + + # Check all paragraphs in the document + for para_idx, paragraph in enumerate(doc.paragraphs): + para_comments = find_paragraph_comments(paragraph, para_idx, comment_id) + comments.extend(para_comments) + comment_id += len(para_comments) + + # Check paragraphs in tables + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para_idx, paragraph in enumerate(cell.paragraphs): + para_comments = find_paragraph_comments(paragraph, para_idx, comment_id, in_table=True) + comments.extend(para_comments) + comment_id += len(para_comments) + + return comments + + +def extract_comment_data(comment_element, index: int) -> Optional[Dict[str, Any]]: + """ + Extract data from a comment XML element. + + Args: + comment_element: The XML comment element + index: Index for generating a unique ID + + Returns: + Dictionary with comment data or None + """ + try: + # Extract comment attributes + comment_id = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}id', str(index)) + author = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}author', 'Unknown') + initials = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}initials', '') + date_str = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}date', '') + + # Parse date if available + date = None + if date_str: + try: + date = datetime.datetime.fromisoformat(date_str.replace('Z', '+00:00')) + date = date.isoformat() + except: + date = date_str + + # Extract comment text + text_elements = comment_element.xpath('.//w:t') + text = ''.join(elem.text or '' for elem in text_elements) + + return { + 'id': f'comment_{index + 1}', + 'comment_id': comment_id, + 'author': author, + 'initials': initials, + 'date': date, + 'text': text.strip(), + 'paragraph_index': None, # Will be filled if we can determine it + 'in_table': False, + 'reference_text': '' + } + + except Exception as e: + return None + + +def find_paragraph_comments(paragraph: Paragraph, para_index: int, + start_id: int, in_table: bool = False) -> List[Dict[str, Any]]: + """ + Find comments associated with a specific paragraph. + + Args: + paragraph: The paragraph to check + para_index: The index of the paragraph + start_id: Starting ID for comments + in_table: Whether the paragraph is in a table + + Returns: + List of comment dictionaries + """ + comments = [] + + try: + # Access the paragraph's XML element + para_xml = paragraph._element + + # Look for comment range markers (simplified approach) + # This is a basic implementation - the full version would need more sophisticated XML parsing + xml_text = str(para_xml) + + # Simple check for comment references in the XML + if 'commentRangeStart' in xml_text or 'commentReference' in xml_text: + # Create a placeholder comment entry + comment_info = { + 'id': f'comment_{start_id}', + 'comment_id': f'{start_id}', + 'author': 'Unknown', + 'initials': '', + 'date': None, + 'text': 'Comment detected but content not accessible', + 'paragraph_index': para_index, + 'in_table': in_table, + 'reference_text': paragraph.text[:50] + '...' if len(paragraph.text) > 50 else paragraph.text + } + comments.append(comment_info) + + except Exception: + # If we can't access the XML, skip this paragraph + pass + + return comments + + +def filter_comments_by_author(comments: List[Dict[str, Any]], author: str) -> List[Dict[str, Any]]: + """ + Filter comments by author name. + + Args: + comments: List of comment dictionaries + author: Author name to filter by (case-insensitive) + + Returns: + Filtered list of comments + """ + author_lower = author.lower() + return [c for c in comments if c.get('author', '').lower() == author_lower] + + +def get_comments_for_paragraph(comments: List[Dict[str, Any]], paragraph_index: int) -> List[Dict[str, Any]]: + """ + Get all comments for a specific paragraph. + + Args: + comments: List of all comments + paragraph_index: Index of the paragraph + + Returns: + Comments for the specified paragraph + """ + return [c for c in comments if c.get('paragraph_index') == paragraph_index] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/footnotes.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/footnotes.py new file mode 100644 index 00000000..f5147e0c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/footnotes.py @@ -0,0 +1,842 @@ +""" +Consolidated footnote functionality for Word documents. +This module combines all footnote implementations with proper namespace handling and Word compliance. +""" + +import os +import zipfile +import tempfile +from typing import Optional, Tuple, Dict, Any, List +from lxml import etree +from docx import Document +from docx.oxml.ns import qn + +# Namespace definitions +W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' +R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' +CT_NS = 'http://schemas.openxmlformats.org/package/2006/content-types' +REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships' + +# Constants +RESERVED_FOOTNOTE_IDS = {-1, 0, 1} # Reserved for separators and Word internals +MIN_FOOTNOTE_ID = -2147483648 +MAX_FOOTNOTE_ID = 32767 +MAX_RELATIONSHIP_ID_LENGTH = 255 +FOOTNOTE_REF_STYLE_INDEX = 38 +FOOTNOTE_TEXT_STYLE_INDEX = 29 + + +# ============================================================================ +# BASIC UTILITIES (from footnotes.py) +# ============================================================================ + +def find_footnote_references(doc): + """Find all footnote references in the document.""" + footnote_refs = [] + for para_idx, para in enumerate(doc.paragraphs): + for run_idx, run in enumerate(para.runs): + # Check if this run has superscript formatting + if run.font.superscript: + # Check if it's likely a footnote reference + if run.text.isdigit() or run.text in "¹²³⁴⁵⁶⁷⁸⁹⁰†‡§¶": + footnote_refs.append({ + 'paragraph_index': para_idx, + 'run_index': run_idx, + 'text': run.text, + 'paragraph': para, + 'run': run + }) + return footnote_refs + + +def get_format_symbols(format_type: str, count: int) -> list: + """Generate format symbols for footnote numbering.""" + symbols = [] + + if format_type == "1, 2, 3": + symbols = [str(i) for i in range(1, count + 1)] + elif format_type == "i, ii, iii": + # Roman numerals + roman_map = [(10, 'x'), (9, 'ix'), (5, 'v'), (4, 'iv'), (1, 'i')] + for i in range(1, count + 1): + result = '' + num = i + for value, numeral in roman_map: + count_sym, num = divmod(num, value) + result += numeral * count_sym + symbols.append(result) + elif format_type == "a, b, c": + # Alphabetic + for i in range(1, count + 1): + if i <= 26: + symbols.append(chr(96 + i)) + else: + # For numbers > 26, use aa, ab, etc. + first = (i - 1) // 26 + second = (i - 1) % 26 + 1 + symbols.append(chr(96 + first) + chr(96 + second)) + elif format_type == "*, †, ‡": + # Special symbols + special = ['*', '†', '‡', '§', '¶', '#'] + for i in range(1, count + 1): + if i <= len(special): + symbols.append(special[i - 1]) + else: + # Repeat symbols with numbers + symbols.append(special[(i - 1) % len(special)] + str((i - 1) // len(special) + 1)) + else: + # Default to numeric + symbols = [str(i) for i in range(1, count + 1)] + + return symbols + + +def customize_footnote_formatting(doc, footnote_refs, format_symbols, start_number, footnote_style): + """Apply custom formatting to footnotes.""" + count = 0 + for i, ref in enumerate(footnote_refs): + if i < len(format_symbols): + # Update the footnote reference text + ref['run'].text = format_symbols[i] + ref['run'].font.superscript = True + + # Apply style if available + if footnote_style: + try: + ref['paragraph'].style = footnote_style + except: + pass + count += 1 + return count + + +# ============================================================================ +# ROBUST IMPLEMENTATION (consolidated from footnotes_robust.py) +# ============================================================================ + +def _get_safe_footnote_id(footnotes_root) -> int: + """Get a safe footnote ID avoiding conflicts and reserved values.""" + nsmap = {'w': W_NS} + existing_footnotes = footnotes_root.xpath('//w:footnote', namespaces=nsmap) + + used_ids = set() + for fn in existing_footnotes: + fn_id = fn.get(f'{{{W_NS}}}id') + if fn_id: + try: + used_ids.add(int(fn_id)) + except ValueError: + pass + + # Start from 2 to avoid reserved IDs + candidate_id = 2 + while candidate_id in used_ids or candidate_id in RESERVED_FOOTNOTE_IDS: + candidate_id += 1 + if candidate_id > MAX_FOOTNOTE_ID: + raise ValueError("No available footnote IDs") + + return candidate_id + + +def _ensure_content_types(content_types_xml: bytes) -> bytes: + """Ensure content types with proper namespace handling.""" + ct_tree = etree.fromstring(content_types_xml) + + # Content Types uses default namespace - must use namespace-aware XPath + nsmap = {'ct': CT_NS} + + # Check for existing override with proper namespace + existing_overrides = ct_tree.xpath( + "//ct:Override[@PartName='/word/footnotes.xml']", + namespaces=nsmap + ) + + if existing_overrides: + return content_types_xml # Already exists + + # Add override with proper namespace + override = etree.Element(f'{{{CT_NS}}}Override', + PartName='/word/footnotes.xml', + ContentType='application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml' + ) + ct_tree.append(override) + + return etree.tostring(ct_tree, encoding='UTF-8', xml_declaration=True, standalone="yes") + + +def _ensure_document_rels(document_rels_xml: bytes) -> bytes: + """Ensure document relationships with proper namespace handling.""" + rels_tree = etree.fromstring(document_rels_xml) + + # Relationships uses default namespace - must use namespace-aware XPath + nsmap = {'r': REL_NS} + + # Check for existing footnotes relationship with proper namespace + existing_footnote_rels = rels_tree.xpath( + "//r:Relationship[contains(@Type, 'footnotes')]", + namespaces=nsmap + ) + + if existing_footnote_rels: + return document_rels_xml # Already exists + + # Generate unique rId using namespace-aware XPath + all_rels = rels_tree.xpath('//r:Relationship', namespaces=nsmap) + existing_ids = {rel.get('Id') for rel in all_rels if rel.get('Id')} + rid_num = 1 + while f'rId{rid_num}' in existing_ids: + rid_num += 1 + + # Validate ID length + new_rid = f'rId{rid_num}' + if len(new_rid) > MAX_RELATIONSHIP_ID_LENGTH: + raise ValueError(f"Relationship ID too long: {new_rid}") + + # Create relationship with proper namespace + rel = etree.Element(f'{{{REL_NS}}}Relationship', + Id=new_rid, + Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes', + Target='footnotes.xml' + ) + rels_tree.append(rel) + + return etree.tostring(rels_tree, encoding='UTF-8', xml_declaration=True, standalone="yes") + + +def _create_minimal_footnotes_xml() -> bytes: + """Create minimal footnotes.xml with separators.""" + xml = f''' + + + + + + + + + + + + + + + + + + + + + +''' + return xml.encode('utf-8') + + +def _ensure_footnote_styles(styles_root): + """Ensure both FootnoteReference and FootnoteText styles exist.""" + nsmap = {'w': W_NS} + + # Check for FootnoteReference style + ref_style = styles_root.xpath('//w:style[@w:styleId="FootnoteReference"]', namespaces=nsmap) + if not ref_style: + # Create FootnoteReference character style + style = etree.Element(f'{{{W_NS}}}style', + attrib={ + f'{{{W_NS}}}type': 'character', + f'{{{W_NS}}}styleId': 'FootnoteReference' + } + ) + name = etree.SubElement(style, f'{{{W_NS}}}name') + name.set(f'{{{W_NS}}}val', 'footnote reference') + + base = etree.SubElement(style, f'{{{W_NS}}}basedOn') + base.set(f'{{{W_NS}}}val', 'DefaultParagraphFont') + + rPr = etree.SubElement(style, f'{{{W_NS}}}rPr') + vert_align = etree.SubElement(rPr, f'{{{W_NS}}}vertAlign') + vert_align.set(f'{{{W_NS}}}val', 'superscript') + + styles_root.append(style) + + # Check for FootnoteText style + text_style = styles_root.xpath('//w:style[@w:styleId="FootnoteText"]', namespaces=nsmap) + if not text_style: + # Create FootnoteText paragraph style + style = etree.Element(f'{{{W_NS}}}style', + attrib={ + f'{{{W_NS}}}type': 'paragraph', + f'{{{W_NS}}}styleId': 'FootnoteText' + } + ) + name = etree.SubElement(style, f'{{{W_NS}}}name') + name.set(f'{{{W_NS}}}val', 'footnote text') + + base = etree.SubElement(style, f'{{{W_NS}}}basedOn') + base.set(f'{{{W_NS}}}val', 'Normal') + + pPr = etree.SubElement(style, f'{{{W_NS}}}pPr') + sz = etree.SubElement(pPr, f'{{{W_NS}}}sz') + sz.set(f'{{{W_NS}}}val', '20') # 10pt + + styles_root.append(style) + + +def add_footnote_robust( + filename: str, + search_text: Optional[str] = None, + paragraph_index: Optional[int] = None, + footnote_text: str = "", + output_filename: Optional[str] = None, + position: str = "after", + validate_location: bool = True, + auto_repair: bool = False +) -> Tuple[bool, str, Optional[Dict[str, Any]]]: + """ + Add a footnote with robust validation and error handling. + + This is the main production-ready function with all fixes applied. + """ + + # Validate inputs + if not search_text and paragraph_index is None: + return False, "Must provide either search_text or paragraph_index", None + + if search_text and paragraph_index is not None: + return False, "Cannot provide both search_text and paragraph_index", None + + if not os.path.exists(filename): + return False, f"File not found: {filename}", None + + # Set working file + working_file = output_filename if output_filename else filename + if output_filename and filename != output_filename: + import shutil + shutil.copy2(filename, output_filename) + + try: + # Read document parts + doc_parts = {} + with zipfile.ZipFile(filename, 'r') as zin: + doc_parts['document'] = zin.read('word/document.xml') + doc_parts['content_types'] = zin.read('[Content_Types].xml') + doc_parts['document_rels'] = zin.read('word/_rels/document.xml.rels') + + # Read or create footnotes.xml + if 'word/footnotes.xml' in zin.namelist(): + doc_parts['footnotes'] = zin.read('word/footnotes.xml') + else: + doc_parts['footnotes'] = _create_minimal_footnotes_xml() + + # Read styles + if 'word/styles.xml' in zin.namelist(): + doc_parts['styles'] = zin.read('word/styles.xml') + else: + # Create minimal styles + doc_parts['styles'] = b'' + + # Parse XML documents + doc_root = etree.fromstring(doc_parts['document']) + footnotes_root = etree.fromstring(doc_parts['footnotes']) + styles_root = etree.fromstring(doc_parts['styles']) + + # Find target location + nsmap = {'w': W_NS} + + if search_text: + # Search for text in paragraphs + found = False + for para in doc_root.xpath('//w:p', namespaces=nsmap): + para_text = ''.join(para.xpath('.//w:t/text()', namespaces=nsmap)) + if search_text in para_text: + target_para = para + found = True + break + + if not found: + return False, f"Text '{search_text}' not found in document", None + else: + # Use paragraph index + paragraphs = doc_root.xpath('//w:p', namespaces=nsmap) + if paragraph_index >= len(paragraphs): + return False, f"Paragraph index {paragraph_index} out of range", None + target_para = paragraphs[paragraph_index] + + # Validate location if requested + if validate_location: + # Check if paragraph is in header/footer + parent = target_para.getparent() + while parent is not None: + if parent.tag in [f'{{{W_NS}}}hdr', f'{{{W_NS}}}ftr']: + return False, "Cannot add footnote in header/footer", None + parent = parent.getparent() + + # Get safe footnote ID + footnote_id = _get_safe_footnote_id(footnotes_root) + + # Add footnote reference to document + if position == "after": + # Find last run in paragraph or create one + runs = target_para.xpath('.//w:r', namespaces=nsmap) + if runs: + last_run = runs[-1] + # Insert after last run + insert_pos = target_para.index(last_run) + 1 + else: + insert_pos = len(target_para) + else: # before + # Find first run with text + runs = target_para.xpath('.//w:r[w:t]', namespaces=nsmap) + if runs: + first_run = runs[0] + insert_pos = target_para.index(first_run) + else: + insert_pos = 0 + + # Create footnote reference run + ref_run = etree.Element(f'{{{W_NS}}}r') + + # Add run properties with superscript + rPr = etree.SubElement(ref_run, f'{{{W_NS}}}rPr') + rStyle = etree.SubElement(rPr, f'{{{W_NS}}}rStyle') + rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference') + + # Add footnote reference + fn_ref = etree.SubElement(ref_run, f'{{{W_NS}}}footnoteReference') + fn_ref.set(f'{{{W_NS}}}id', str(footnote_id)) + + # Insert the reference run + target_para.insert(insert_pos, ref_run) + + # Add footnote content + new_footnote = etree.Element(f'{{{W_NS}}}footnote', + attrib={f'{{{W_NS}}}id': str(footnote_id)} + ) + + # Add paragraph to footnote + fn_para = etree.SubElement(new_footnote, f'{{{W_NS}}}p') + + # Add paragraph properties + pPr = etree.SubElement(fn_para, f'{{{W_NS}}}pPr') + pStyle = etree.SubElement(pPr, f'{{{W_NS}}}pStyle') + pStyle.set(f'{{{W_NS}}}val', 'FootnoteText') + + # Add the footnote reference marker + marker_run = etree.SubElement(fn_para, f'{{{W_NS}}}r') + marker_rPr = etree.SubElement(marker_run, f'{{{W_NS}}}rPr') + marker_rStyle = etree.SubElement(marker_rPr, f'{{{W_NS}}}rStyle') + marker_rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference') + marker_ref = etree.SubElement(marker_run, f'{{{W_NS}}}footnoteRef') + + # Add space after marker + space_run = etree.SubElement(fn_para, f'{{{W_NS}}}r') + space_text = etree.SubElement(space_run, f'{{{W_NS}}}t') + space_text.set(f'{{{XML_NS}}}space', 'preserve') + space_text.text = ' ' + + # Add footnote text + text_run = etree.SubElement(fn_para, f'{{{W_NS}}}r') + text_elem = etree.SubElement(text_run, f'{{{W_NS}}}t') + text_elem.text = footnote_text + + # Append footnote to footnotes.xml + footnotes_root.append(new_footnote) + + # Ensure styles exist + _ensure_footnote_styles(styles_root) + + # Ensure coherence + content_types_xml = _ensure_content_types(doc_parts['content_types']) + document_rels_xml = _ensure_document_rels(doc_parts['document_rels']) + + # Write modified document + temp_file = working_file + '.tmp' + with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zout: + with zipfile.ZipFile(filename, 'r') as zin: + # Copy unchanged files + for item in zin.infolist(): + if item.filename not in [ + 'word/document.xml', 'word/footnotes.xml', 'word/styles.xml', + '[Content_Types].xml', 'word/_rels/document.xml.rels' + ]: + zout.writestr(item, zin.read(item.filename)) + + # Write modified files + zout.writestr('word/document.xml', + etree.tostring(doc_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) + zout.writestr('word/footnotes.xml', + etree.tostring(footnotes_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) + zout.writestr('word/styles.xml', + etree.tostring(styles_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) + zout.writestr('[Content_Types].xml', content_types_xml) + zout.writestr('word/_rels/document.xml.rels', document_rels_xml) + + # Replace original with temp file + os.replace(temp_file, working_file) + + details = { + 'footnote_id': footnote_id, + 'location': 'search_text' if search_text else 'paragraph_index', + 'styles_created': ['FootnoteReference', 'FootnoteText'], + 'coherence_verified': True + } + + return True, f"Successfully added footnote (ID: {footnote_id}) to {working_file}", details + + except Exception as e: + # Clean up temp file if exists + temp_file = working_file + '.tmp' + if os.path.exists(temp_file): + os.remove(temp_file) + return False, f"Error adding footnote: {str(e)}", None + + +def delete_footnote_robust( + filename: str, + footnote_id: Optional[int] = None, + search_text: Optional[str] = None, + output_filename: Optional[str] = None, + clean_orphans: bool = True +) -> Tuple[bool, str, Optional[Dict[str, Any]]]: + """Delete a footnote with comprehensive cleanup.""" + + if not footnote_id and not search_text: + return False, "Must provide either footnote_id or search_text", None + + if not os.path.exists(filename): + return False, f"File not found: {filename}", None + + # Set working file + working_file = output_filename if output_filename else filename + if output_filename and filename != output_filename: + import shutil + shutil.copy2(filename, output_filename) + + try: + # Read document parts + with zipfile.ZipFile(filename, 'r') as zin: + doc_xml = zin.read('word/document.xml') + + if 'word/footnotes.xml' not in zin.namelist(): + return False, "No footnotes in document", None + + footnotes_xml = zin.read('word/footnotes.xml') + + # Parse documents + doc_root = etree.fromstring(doc_xml) + footnotes_root = etree.fromstring(footnotes_xml) + nsmap = {'w': W_NS} + + # Find footnote to delete + if search_text: + # Find footnote reference near text + for para in doc_root.xpath('//w:p', namespaces=nsmap): + para_text = ''.join(para.xpath('.//w:t/text()', namespaces=nsmap)) + if search_text in para_text: + # Look for footnote reference in this paragraph + fn_refs = para.xpath('.//w:footnoteReference', namespaces=nsmap) + if fn_refs: + footnote_id = int(fn_refs[0].get(f'{{{W_NS}}}id')) + break + + if not footnote_id: + return False, f"No footnote found near text '{search_text}'", None + + # Remove footnote reference from document + refs_removed = 0 + for fn_ref in doc_root.xpath(f'//w:footnoteReference[@w:id="{footnote_id}"]', namespaces=nsmap): + # Remove the entire run containing the reference + run = fn_ref.getparent() + if run is not None and run.tag == f'{{{W_NS}}}r': + para = run.getparent() + if para is not None: + para.remove(run) + refs_removed += 1 + + if refs_removed == 0: + return False, f"Footnote {footnote_id} not found", None + + # Remove footnote content + content_removed = 0 + for fn in footnotes_root.xpath(f'//w:footnote[@w:id="{footnote_id}"]', namespaces=nsmap): + footnotes_root.remove(fn) + content_removed += 1 + + # Clean orphans if requested + orphans_removed = [] + if clean_orphans: + # Find all referenced IDs + referenced_ids = set() + for ref in doc_root.xpath('//w:footnoteReference', namespaces=nsmap): + ref_id = ref.get(f'{{{W_NS}}}id') + if ref_id: + referenced_ids.add(ref_id) + + # Remove unreferenced footnotes (except separators) + for fn in footnotes_root.xpath('//w:footnote', namespaces=nsmap): + fn_id = fn.get(f'{{{W_NS}}}id') + if fn_id and fn_id not in referenced_ids and fn_id not in ['-1', '0']: + footnotes_root.remove(fn) + orphans_removed.append(fn_id) + + # Write modified document + temp_file = working_file + '.tmp' + with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zout: + with zipfile.ZipFile(filename, 'r') as zin: + for item in zin.infolist(): + if item.filename == 'word/document.xml': + zout.writestr(item, + etree.tostring(doc_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) + elif item.filename == 'word/footnotes.xml': + zout.writestr(item, + etree.tostring(footnotes_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) + else: + zout.writestr(item, zin.read(item.filename)) + + os.replace(temp_file, working_file) + + details = { + 'footnote_id': footnote_id, + 'references_removed': refs_removed, + 'content_removed': content_removed, + 'orphans_removed': orphans_removed + } + + message = f"Successfully deleted footnote {footnote_id}" + if orphans_removed: + message += f" and {len(orphans_removed)} orphaned footnotes" + + return True, message, details + + except Exception as e: + return False, f"Error deleting footnote: {str(e)}", None + + +def validate_document_footnotes(filename: str) -> Tuple[bool, str, Dict[str, Any]]: + """Validate all footnotes in a document for coherence and compliance.""" + + if not os.path.exists(filename): + return False, f"File not found: {filename}", {} + + report = { + 'total_references': 0, + 'total_content': 0, + 'id_conflicts': [], + 'orphaned_content': [], + 'missing_references': [], + 'invalid_locations': [], + 'missing_styles': [], + 'coherence_issues': [] + } + + try: + with zipfile.ZipFile(filename, 'r') as zf: + # Check document.xml + doc_xml = zf.read('word/document.xml') + doc_root = etree.fromstring(doc_xml) + nsmap = {'w': W_NS} + + # Get all footnote references + ref_ids = set() + for ref in doc_root.xpath('//w:footnoteReference', namespaces=nsmap): + ref_id = ref.get(f'{{{W_NS}}}id') + if ref_id: + ref_ids.add(ref_id) + report['total_references'] += 1 + + # Check location + parent = ref.getparent() + while parent is not None: + if parent.tag in [f'{{{W_NS}}}hdr', f'{{{W_NS}}}ftr']: + report['invalid_locations'].append(ref_id) + break + parent = parent.getparent() + + # Check footnotes.xml + if 'word/footnotes.xml' in zf.namelist(): + footnotes_xml = zf.read('word/footnotes.xml') + footnotes_root = etree.fromstring(footnotes_xml) + + content_ids = set() + for fn in footnotes_root.xpath('//w:footnote', namespaces=nsmap): + fn_id = fn.get(f'{{{W_NS}}}id') + if fn_id: + content_ids.add(fn_id) + if fn_id not in ['-1', '0']: # Exclude separators + report['total_content'] += 1 + + # Find orphans and missing + report['orphaned_content'] = list(content_ids - ref_ids - {'-1', '0'}) + report['missing_references'] = list(ref_ids - content_ids) + else: + if report['total_references'] > 0: + report['coherence_issues'].append('References exist but no footnotes.xml') + + # Check relationships + if 'word/_rels/document.xml.rels' in zf.namelist(): + rels_xml = zf.read('word/_rels/document.xml.rels') + rels_root = etree.fromstring(rels_xml) + rel_nsmap = {'r': REL_NS} + + fn_rels = rels_root.xpath( + "//r:Relationship[contains(@Type, 'footnotes')]", + namespaces=rel_nsmap + ) + + if report['total_content'] > 0 and len(fn_rels) == 0: + report['coherence_issues'].append('Missing footnotes relationship') + elif len(fn_rels) > 1: + report['coherence_issues'].append(f'Multiple footnote relationships: {len(fn_rels)}') + + # Check content types + if '[Content_Types].xml' in zf.namelist(): + ct_xml = zf.read('[Content_Types].xml') + ct_root = etree.fromstring(ct_xml) + ct_nsmap = {'ct': CT_NS} + + fn_overrides = ct_root.xpath( + "//ct:Override[@PartName='/word/footnotes.xml']", + namespaces=ct_nsmap + ) + + if report['total_content'] > 0 and len(fn_overrides) == 0: + report['coherence_issues'].append('Missing footnotes content type') + elif len(fn_overrides) > 1: + report['coherence_issues'].append(f'Multiple footnote content types: {len(fn_overrides)}') + + # Check styles + if 'word/styles.xml' in zf.namelist(): + styles_xml = zf.read('word/styles.xml') + styles_root = etree.fromstring(styles_xml) + + ref_style = styles_root.xpath('//w:style[@w:styleId="FootnoteReference"]', namespaces=nsmap) + text_style = styles_root.xpath('//w:style[@w:styleId="FootnoteText"]', namespaces=nsmap) + + if not ref_style: + report['missing_styles'].append('FootnoteReference') + if not text_style: + report['missing_styles'].append('FootnoteText') + + # Determine if valid + is_valid = ( + len(report['id_conflicts']) == 0 and + len(report['orphaned_content']) == 0 and + len(report['missing_references']) == 0 and + len(report['invalid_locations']) == 0 and + len(report['coherence_issues']) == 0 + ) + + if is_valid: + message = "Document footnotes are valid" + else: + message = "Document has footnote issues" + + return is_valid, message, report + + except Exception as e: + return False, f"Error validating document: {str(e)}", report + + +# ============================================================================ +# COMPATIBILITY FUNCTIONS (for backward compatibility) +# ============================================================================ + +def add_footnote_at_paragraph_end( + filename: str, + paragraph_index: int, + footnote_text: str, + output_filename: Optional[str] = None +) -> Tuple[bool, str]: + """Add footnote at the end of a specific paragraph (backward compatibility).""" + success, message, _ = add_footnote_robust( + filename=filename, + paragraph_index=paragraph_index, + footnote_text=footnote_text, + output_filename=output_filename, + position="after" + ) + return success, message + + +def add_footnote_with_proper_formatting( + filename: str, + search_text: str, + footnote_text: str, + output_filename: Optional[str] = None, + position: str = "after" +) -> Tuple[bool, str]: + """Add footnote with proper formatting (backward compatibility).""" + success, message, _ = add_footnote_robust( + filename=filename, + search_text=search_text, + footnote_text=footnote_text, + output_filename=output_filename, + position=position + ) + return success, message + + +def delete_footnote( + filename: str, + footnote_id: Optional[int] = None, + search_text: Optional[str] = None, + output_filename: Optional[str] = None +) -> Tuple[bool, str]: + """Delete a footnote (backward compatibility).""" + success, message, _ = delete_footnote_robust( + filename=filename, + footnote_id=footnote_id, + search_text=search_text, + output_filename=output_filename + ) + return success, message + + +# ============================================================================ +# LEGACY FUNCTIONS (for core/__init__.py compatibility) +# ============================================================================ + +def add_footnote(doc, paragraph_index: int, footnote_text: str): + """Legacy function for adding footnotes to python-docx Document objects. + Note: This is a simplified version that doesn't create proper Word footnotes.""" + if paragraph_index >= len(doc.paragraphs): + raise IndexError(f"Paragraph index {paragraph_index} out of range") + + para = doc.paragraphs[paragraph_index] + # Add superscript number + run = para.add_run() + run.text = "¹" + run.font.superscript = True + + # Add footnote text at document end + doc.add_paragraph("_" * 50) + footnote_para = doc.add_paragraph(f"¹ {footnote_text}") + footnote_para.style = "Caption" + + return doc + + +def add_endnote(doc, paragraph_index: int, endnote_text: str): + """Legacy function for adding endnotes.""" + if paragraph_index >= len(doc.paragraphs): + raise IndexError(f"Paragraph index {paragraph_index} out of range") + + para = doc.paragraphs[paragraph_index] + run = para.add_run() + run.text = "†" + run.font.superscript = True + + # Endnotes go at the very end + doc.add_page_break() + doc.add_heading("Endnotes", level=1) + endnote_para = doc.add_paragraph(f"† {endnote_text}") + + return doc + + +def convert_footnotes_to_endnotes(doc): + """Legacy function to convert footnotes to endnotes in a Document object.""" + # This is a placeholder - real conversion requires XML manipulation + return doc + + +# Define XML_NS if needed +XML_NS = 'http://www.w3.org/XML/1998/namespace' \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/protection.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/protection.py new file mode 100644 index 00000000..e706fd79 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/protection.py @@ -0,0 +1,242 @@ +""" +Document protection functionality for Word Document Server. +""" +import os +import json +import hashlib +import datetime +from typing import Dict, List, Tuple, Optional, Any + + +def add_protection_info(doc_path: str, protection_type: str, password_hash: str, + sections: Optional[List[str]] = None, + signature_info: Optional[Dict[str, Any]] = None, + raw_password: Optional[str] = None) -> bool: + """ + Add document protection information to a separate metadata file and encrypt the document. + + Args: + doc_path: Path to the document + protection_type: Type of protection ('password', 'restricted', 'signature') + password_hash: Hashed password for security + sections: List of section names that can be edited (for restricted editing) + signature_info: Information about digital signature + raw_password: The actual password for document encryption + + Returns: + True if protection info was successfully added, False otherwise + """ + # Create metadata filename based on document path + base_path, _ = os.path.splitext(doc_path) + metadata_path = f"{base_path}.protection" + + # Prepare protection data + protection_data = { + "type": protection_type, + "password_hash": password_hash, + "applied_date": datetime.datetime.now().isoformat(), + } + + if sections: + protection_data["editable_sections"] = sections + + if signature_info: + protection_data["signature"] = signature_info + + # Write protection info to metadata file + try: + with open(metadata_path, 'w') as f: + json.dump(protection_data, f, indent=2) + + # Apply actual document encryption if raw_password is provided + if protection_type == "password" and raw_password: + import msoffcrypto + import tempfile + import shutil + + # Create a temporary file for the encrypted output + temp_fd, temp_path = tempfile.mkstemp(suffix='.docx') + os.close(temp_fd) + + try: + # Open the document + with open(doc_path, 'rb') as f: + office_file = msoffcrypto.OfficeFile(f) + + # Encrypt with password + office_file.load_key(password=raw_password) + + # Write the encrypted file to the temp path + with open(temp_path, 'wb') as out_file: + office_file.encrypt(out_file) + + # Replace original with encrypted version + shutil.move(temp_path, doc_path) + + # Update metadata to note that true encryption was applied + protection_data["true_encryption"] = True + with open(metadata_path, 'w') as f: + json.dump(protection_data, f, indent=2) + + except Exception as e: + print(f"Encryption error: {str(e)}") + if os.path.exists(temp_path): + os.unlink(temp_path) + return False + + return True + except Exception as e: + print(f"Protection error: {str(e)}") + return False + + +def verify_document_protection(doc_path: str, password: Optional[str] = None) -> Tuple[bool, str]: + """ + Verify if a document is protected and if the password is correct. + + Args: + doc_path: Path to the document + password: Password to verify + + Returns: + Tuple of (is_protected_and_verified, message) + """ + base_path, _ = os.path.splitext(doc_path) + metadata_path = f"{base_path}.protection" + + # Check if protection metadata exists + if not os.path.exists(metadata_path): + return False, "Document is not protected" + + try: + # Read protection data + with open(metadata_path, 'r') as f: + protection_data = json.load(f) + + # If password is provided, verify it + if password: + password_hash = hashlib.sha256(password.encode()).hexdigest() + if password_hash != protection_data.get("password_hash"): + return False, "Incorrect password" + + # Return protection type + protection_type = protection_data.get("type", "unknown") + return True, f"Document is protected with {protection_type} protection" + + except Exception as e: + return False, f"Error verifying protection: {str(e)}" + + +def is_section_editable(doc_path: str, section_name: str) -> bool: + """ + Check if a specific section of a document is editable. + + Args: + doc_path: Path to the document + section_name: Name of the section to check + + Returns: + True if section is editable, False otherwise + """ + base_path, _ = os.path.splitext(doc_path) + metadata_path = f"{base_path}.protection" + + # Check if protection metadata exists + if not os.path.exists(metadata_path): + # If no protection exists, all sections are editable + return True + + try: + # Read protection data + with open(metadata_path, 'r') as f: + protection_data = json.load(f) + + # Check protection type + if protection_data.get("type") != "restricted": + # If not restricted editing, return based on protection type + return protection_data.get("type") != "password" + + # Check if the section is in the list of editable sections + editable_sections = protection_data.get("editable_sections", []) + return section_name in editable_sections + + except Exception: + # In case of error, default to not editable for security + return False + + +def create_signature_info(doc, signer_name: str, reason: Optional[str] = None) -> Dict[str, Any]: + """ + Create signature information for a document. + + Args: + doc: Document object + signer_name: Name of the person signing the document + reason: Optional reason for signing + + Returns: + Dictionary containing signature information + """ + # Create signature info + signature_info = { + "signer": signer_name, + "timestamp": datetime.datetime.now().isoformat(), + } + + if reason: + signature_info["reason"] = reason + + # Generate a simple signature hash based on document content and metadata + text_content = "\n".join([p.text for p in doc.paragraphs]) + content_hash = hashlib.sha256(text_content.encode()).hexdigest() + signature_info["content_hash"] = content_hash + + return signature_info + + +def verify_signature(doc_path: str) -> Tuple[bool, str]: + """ + Verify a document's digital signature. + + Args: + doc_path: Path to the document + + Returns: + Tuple of (is_valid, message) + """ + from docx import Document + + base_path, _ = os.path.splitext(doc_path) + metadata_path = f"{base_path}.protection" + + if not os.path.exists(metadata_path): + return False, "Document is not signed" + + try: + # Read protection data + with open(metadata_path, 'r') as f: + protection_data = json.load(f) + + if protection_data.get("type") != "signature": + return False, f"Document is protected with {protection_data.get('type')} protection, not a signature" + + # Get the original content hash + signature_info = protection_data.get("signature", {}) + original_hash = signature_info.get("content_hash") + + if not original_hash: + return False, "Invalid signature: missing content hash" + + # Calculate current content hash + doc = Document(doc_path) + text_content = "\n".join([p.text for p in doc.paragraphs]) + current_hash = hashlib.sha256(text_content.encode()).hexdigest() + + # Compare hashes + if current_hash != original_hash: + return False, f"Document has been modified since it was signed by {signature_info.get('signer')}" + + return True, f"Document signature is valid. Signed by {signature_info.get('signer')} on {signature_info.get('timestamp')}" + + except Exception as e: + return False, f"Error verifying signature: {str(e)}" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/styles.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/styles.py new file mode 100644 index 00000000..49c5d51f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/styles.py @@ -0,0 +1,134 @@ +""" +Style-related functions for Word Document Server. +""" +from docx.shared import Pt +from docx.enum.style import WD_STYLE_TYPE + + +def ensure_heading_style(doc): + """ + Ensure Heading styles exist in the document. + + Args: + doc: Document object + """ + for i in range(1, 10): # Create Heading 1 through Heading 9 + style_name = f'Heading {i}' + try: + # Try to access the style to see if it exists + style = doc.styles[style_name] + except KeyError: + # Create the style if it doesn't exist + try: + style = doc.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH) + if i == 1: + style.font.size = Pt(16) + style.font.bold = True + elif i == 2: + style.font.size = Pt(14) + style.font.bold = True + else: + style.font.size = Pt(12) + style.font.bold = True + except Exception: + # If style creation fails, we'll just use default formatting + pass + + +def ensure_table_style(doc): + """ + Ensure Table Grid style exists in the document. + + Args: + doc: Document object + """ + try: + # Try to access the style to see if it exists + style = doc.styles['Table Grid'] + except KeyError: + # If style doesn't exist, we'll handle it at usage time + pass + + +def create_style(doc, style_name, style_type, base_style=None, font_properties=None, paragraph_properties=None): + """ + Create a new style in the document. + + Args: + doc: Document object + style_name: Name for the new style + style_type: Type of style (WD_STYLE_TYPE) + base_style: Optional base style to inherit from + font_properties: Dictionary of font properties (bold, italic, size, name, color) + paragraph_properties: Dictionary of paragraph properties (alignment, spacing) + + Returns: + The created style + """ + from docx.shared import Pt + + try: + # Check if style already exists + style = doc.styles.get_by_id(style_name, WD_STYLE_TYPE.PARAGRAPH) + return style + except: + # Create new style + new_style = doc.styles.add_style(style_name, style_type) + + # Set base style if specified + if base_style: + new_style.base_style = doc.styles[base_style] + + # Set font properties + if font_properties: + font = new_style.font + if 'bold' in font_properties: + font.bold = font_properties['bold'] + if 'italic' in font_properties: + font.italic = font_properties['italic'] + if 'size' in font_properties: + font.size = Pt(font_properties['size']) + if 'name' in font_properties: + font.name = font_properties['name'] + if 'color' in font_properties: + from docx.shared import RGBColor + + # Define common RGB colors + color_map = { + 'red': RGBColor(255, 0, 0), + 'blue': RGBColor(0, 0, 255), + 'green': RGBColor(0, 128, 0), + 'yellow': RGBColor(255, 255, 0), + 'black': RGBColor(0, 0, 0), + 'gray': RGBColor(128, 128, 128), + 'white': RGBColor(255, 255, 255), + 'purple': RGBColor(128, 0, 128), + 'orange': RGBColor(255, 165, 0) + } + + color_value = font_properties['color'] + try: + # Handle string color names + if isinstance(color_value, str) and color_value.lower() in color_map: + font.color.rgb = color_map[color_value.lower()] + # Handle RGBColor objects + elif hasattr(color_value, 'rgb'): + font.color.rgb = color_value + # Try to parse as RGB string + elif isinstance(color_value, str): + font.color.rgb = RGBColor.from_string(color_value) + # Use directly if it's already an RGB value + else: + font.color.rgb = color_value + except Exception as e: + # Fallback to black if all else fails + font.color.rgb = RGBColor(0, 0, 0) + + # Set paragraph properties + if paragraph_properties: + if 'alignment' in paragraph_properties: + new_style.paragraph_format.alignment = paragraph_properties['alignment'] + if 'spacing' in paragraph_properties: + new_style.paragraph_format.line_spacing = paragraph_properties['spacing'] + + return new_style diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/tables.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/tables.py new file mode 100644 index 00000000..9cc8dcf6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/tables.py @@ -0,0 +1,866 @@ +""" +Table-related operations for Word Document Server. +""" +from docx.oxml.shared import OxmlElement, qn +from docx.oxml.ns import nsdecls +from docx.oxml import parse_xml +from docx.shared import RGBColor, Inches, Cm, Pt +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT + + +def set_cell_border(cell, **kwargs): + """ + Set cell border properties. + + Args: + cell: The cell to modify + **kwargs: Border properties (top, bottom, left, right, val, color) + """ + tc = cell._tc + tcPr = tc.get_or_add_tcPr() + + # Create border elements + for key, value in kwargs.items(): + if key in ['top', 'left', 'bottom', 'right']: + tag = 'w:{}'.format(key) + + element = OxmlElement(tag) + element.set(qn('w:val'), kwargs.get('val', 'single')) + element.set(qn('w:sz'), kwargs.get('sz', '4')) + element.set(qn('w:space'), kwargs.get('space', '0')) + element.set(qn('w:color'), kwargs.get('color', 'auto')) + + tcBorders = tcPr.first_child_found_in("w:tcBorders") + if tcBorders is None: + tcBorders = OxmlElement('w:tcBorders') + tcPr.append(tcBorders) + + tcBorders.append(element) + + +def apply_table_style(table, has_header_row=False, border_style=None, shading=None): + """ + Apply formatting to a table. + + Args: + table: The table to format + has_header_row: If True, formats the first row as a header + border_style: Style for borders ('none', 'single', 'double', 'thick') + shading: 2D list of cell background colors (by row and column) + + Returns: + True if successful, False otherwise + """ + try: + # Format header row if requested + if has_header_row and table.rows: + header_row = table.rows[0] + for cell in header_row.cells: + for paragraph in cell.paragraphs: + if paragraph.runs: + for run in paragraph.runs: + run.bold = True + + # Apply border style if specified + if border_style: + val_map = { + 'none': 'nil', + 'single': 'single', + 'double': 'double', + 'thick': 'thick' + } + val = val_map.get(border_style.lower(), 'single') + + # Apply to all cells + for row in table.rows: + for cell in row.cells: + set_cell_border( + cell, + top=True, + bottom=True, + left=True, + right=True, + val=val, + color="000000" + ) + + # Apply cell shading if specified + if shading: + for i, row_colors in enumerate(shading): + if i >= len(table.rows): + break + for j, color in enumerate(row_colors): + if j >= len(table.rows[i].cells): + break + try: + # Apply shading to cell + cell = table.rows[i].cells[j] + shading_elm = parse_xml(f'') + cell._tc.get_or_add_tcPr().append(shading_elm) + except: + # Skip if color format is invalid + pass + + return True + except Exception: + return False + + +def copy_table(source_table, target_doc): + """ + Copy a table from one document to another. + + Args: + source_table: The table to copy + target_doc: The document to copy the table to + + Returns: + The new table in the target document + """ + # Create a new table with the same dimensions + new_table = target_doc.add_table(rows=len(source_table.rows), cols=len(source_table.columns)) + + # Try to apply the same style + try: + if source_table.style: + new_table.style = source_table.style + except: + # Fall back to default grid style + try: + new_table.style = 'Table Grid' + except: + pass + + # Copy cell contents + for i, row in enumerate(source_table.rows): + for j, cell in enumerate(row.cells): + for paragraph in cell.paragraphs: + if paragraph.text: + new_table.cell(i, j).text = paragraph.text + + return new_table + + +def set_cell_shading(cell, fill_color=None, pattern="clear", pattern_color="auto"): + """ + Apply shading/filling to a table cell. + + Args: + cell: The table cell to format + fill_color: Background color (hex string like "FF0000" or RGBColor) + pattern: Shading pattern ("clear", "solid", "pct10", "pct20", etc.) + pattern_color: Pattern color for patterned fills + + Returns: + True if successful, False otherwise + """ + try: + # Get or create table cell properties + tc_pr = cell._tc.get_or_add_tcPr() + + # Remove existing shading + existing_shd = tc_pr.find(qn('w:shd')) + if existing_shd is not None: + tc_pr.remove(existing_shd) + + # Create shading element + shd_attrs = { + 'w:val': pattern, + 'w:color': pattern_color if pattern_color != "auto" else "auto" + } + + # Set fill color + if fill_color: + if isinstance(fill_color, str): + # Hex color string - remove # if present + fill_color = fill_color.lstrip('#').upper() + if len(fill_color) == 6: # Valid hex color + shd_attrs['w:fill'] = fill_color + elif isinstance(fill_color, RGBColor): + # RGBColor object + hex_color = f"{fill_color.r:02X}{fill_color.g:02X}{fill_color.b:02X}" + shd_attrs['w:fill'] = hex_color + + # Build XML string + attr_str = ' '.join([f'{k}="{v}"' for k, v in shd_attrs.items()]) + shd_xml = f'' + + # Parse and append shading element + shading_elm = parse_xml(shd_xml) + tc_pr.append(shading_elm) + + return True + + except Exception as e: + print(f"Error setting cell shading: {e}") + return False + + +def apply_alternating_row_shading(table, color1="FFFFFF", color2="F2F2F2"): + """ + Apply alternating row colors for better readability. + + Args: + table: The table to format + color1: Color for odd rows (hex string) + color2: Color for even rows (hex string) + + Returns: + True if successful, False otherwise + """ + try: + for i, row in enumerate(table.rows): + fill_color = color1 if i % 2 == 0 else color2 + for cell in row.cells: + set_cell_shading(cell, fill_color=fill_color) + return True + except Exception as e: + print(f"Error applying alternating row shading: {e}") + return False + + +def highlight_header_row(table, header_color="4472C4", text_color="FFFFFF"): + """ + Apply special shading to header row. + + Args: + table: The table to format + header_color: Background color for header (hex string) + text_color: Text color for header (hex string) + + Returns: + True if successful, False otherwise + """ + try: + if table.rows: + for cell in table.rows[0].cells: + # Apply background shading + set_cell_shading(cell, fill_color=header_color) + + # Apply text formatting + for paragraph in cell.paragraphs: + for run in paragraph.runs: + run.bold = True + if text_color and text_color != "auto": + # Convert hex to RGB + try: + text_color = text_color.lstrip('#') + r = int(text_color[0:2], 16) + g = int(text_color[2:4], 16) + b = int(text_color[4:6], 16) + run.font.color.rgb = RGBColor(r, g, b) + except: + pass # Skip if color format is invalid + return True + except Exception as e: + print(f"Error highlighting header row: {e}") + return False + + +def set_cell_shading_by_position(table, row_index, col_index, fill_color, pattern="clear"): + """ + Apply shading to a specific cell by row/column position. + + Args: + table: The table containing the cell + row_index: Row index (0-based) + col_index: Column index (0-based) + fill_color: Background color (hex string) + pattern: Shading pattern + + Returns: + True if successful, False otherwise + """ + try: + if (0 <= row_index < len(table.rows) and + 0 <= col_index < len(table.rows[row_index].cells)): + cell = table.rows[row_index].cells[col_index] + return set_cell_shading(cell, fill_color=fill_color, pattern=pattern) + else: + return False + except Exception as e: + print(f"Error setting cell shading by position: {e}") + return False + + +def merge_cells(table, start_row, start_col, end_row, end_col): + """ + Merge cells in a rectangular area. + + Args: + table: The table containing cells to merge + start_row: Starting row index (0-based) + start_col: Starting column index (0-based) + end_row: Ending row index (0-based, inclusive) + end_col: Ending column index (0-based, inclusive) + + Returns: + True if successful, False otherwise + """ + try: + # Validate indices + if (start_row < 0 or start_col < 0 or end_row < 0 or end_col < 0 or + start_row >= len(table.rows) or end_row >= len(table.rows) or + start_row > end_row or start_col > end_col): + return False + + # Check if all rows have enough columns + for row_idx in range(start_row, end_row + 1): + if (start_col >= len(table.rows[row_idx].cells) or + end_col >= len(table.rows[row_idx].cells)): + return False + + # Get the start and end cells + start_cell = table.cell(start_row, start_col) + end_cell = table.cell(end_row, end_col) + + # Merge the cells + start_cell.merge(end_cell) + + return True + + except Exception as e: + print(f"Error merging cells: {e}") + return False + + +def merge_cells_horizontal(table, row_index, start_col, end_col): + """ + Merge cells horizontally in a single row. + + Args: + table: The table containing cells to merge + row_index: Row index (0-based) + start_col: Starting column index (0-based) + end_col: Ending column index (0-based, inclusive) + + Returns: + True if successful, False otherwise + """ + return merge_cells(table, row_index, start_col, row_index, end_col) + + +def merge_cells_vertical(table, col_index, start_row, end_row): + """ + Merge cells vertically in a single column. + + Args: + table: The table containing cells to merge + col_index: Column index (0-based) + start_row: Starting row index (0-based) + end_row: Ending row index (0-based, inclusive) + + Returns: + True if successful, False otherwise + """ + return merge_cells(table, start_row, col_index, end_row, col_index) + + +def set_cell_alignment(cell, horizontal="left", vertical="top"): + """ + Set text alignment within a cell. + + Args: + cell: The table cell to format + horizontal: Horizontal alignment ("left", "center", "right", "justify") + vertical: Vertical alignment ("top", "center", "bottom") + + Returns: + True if successful, False otherwise + """ + try: + # Set horizontal alignment for all paragraphs in the cell + for paragraph in cell.paragraphs: + if horizontal.lower() == "center": + paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + elif horizontal.lower() == "right": + paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT + elif horizontal.lower() == "justify": + paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY + else: # default to left + paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT + + # Set vertical alignment for the cell using XML manipulation + tc_pr = cell._tc.get_or_add_tcPr() + + # Remove existing vertical alignment + existing_valign = tc_pr.find(qn('w:vAlign')) + if existing_valign is not None: + tc_pr.remove(existing_valign) + + # Create vertical alignment element + valign_element = OxmlElement('w:vAlign') + if vertical.lower() == "center": + valign_element.set(qn('w:val'), 'center') + elif vertical.lower() == "bottom": + valign_element.set(qn('w:val'), 'bottom') + else: # default to top + valign_element.set(qn('w:val'), 'top') + + tc_pr.append(valign_element) + + return True + + except Exception as e: + print(f"Error setting cell alignment: {e}") + return False + + +def set_cell_alignment_by_position(table, row_index, col_index, horizontal="left", vertical="top"): + """ + Set text alignment for a specific cell by position. + + Args: + table: The table containing the cell + row_index: Row index (0-based) + col_index: Column index (0-based) + horizontal: Horizontal alignment ("left", "center", "right", "justify") + vertical: Vertical alignment ("top", "center", "bottom") + + Returns: + True if successful, False otherwise + """ + try: + if (0 <= row_index < len(table.rows) and + 0 <= col_index < len(table.rows[row_index].cells)): + cell = table.rows[row_index].cells[col_index] + return set_cell_alignment(cell, horizontal, vertical) + else: + return False + except Exception as e: + print(f"Error setting cell alignment by position: {e}") + return False + + +def set_table_alignment(table, horizontal="left", vertical="top"): + """ + Set text alignment for all cells in a table. + + Args: + table: The table to format + horizontal: Horizontal alignment ("left", "center", "right", "justify") + vertical: Vertical alignment ("top", "center", "bottom") + + Returns: + True if successful, False otherwise + """ + try: + for row in table.rows: + for cell in row.cells: + set_cell_alignment(cell, horizontal, vertical) + return True + except Exception as e: + print(f"Error setting table alignment: {e}") + return False + + +def set_column_width(table, col_index, width, width_type="dxa"): + """ + Set the width of a specific column in a table. + + Args: + table: The table to modify + col_index: Column index (0-based) + width: Column width value + width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto") + + Returns: + True if successful, False otherwise + """ + try: + # Validate column index + if col_index < 0 or col_index >= len(table.columns): + return False + + # Convert width based on type + if width_type == "dxa": + # DXA units (twentieths of a point) + if isinstance(width, (int, float)): + width_value = str(int(width * 20)) + else: + width_value = str(width) + elif width_type == "pct": + # Percentage (multiply by 50 for Word format) + if isinstance(width, (int, float)): + width_value = str(int(width * 50)) + else: + width_value = str(width) + else: + width_value = str(width) + + # Iterate through all rows and set width for cells in the specified column + for row in table.rows: + if col_index < len(row.cells): + cell = row.cells[col_index] + tc_pr = cell._tc.get_or_add_tcPr() + + # Remove existing width + existing_width = tc_pr.find(qn('w:tcW')) + if existing_width is not None: + tc_pr.remove(existing_width) + + # Create new width element + width_element = OxmlElement('w:tcW') + width_element.set(qn('w:w'), width_value) + width_element.set(qn('w:type'), width_type) + + tc_pr.append(width_element) + + return True + + except Exception as e: + print(f"Error setting column width: {e}") + return False + + +def set_column_width_by_position(table, col_index, width, width_type="dxa"): + """ + Set the width of a specific column by position. + + Args: + table: The table containing the column + col_index: Column index (0-based) + width: Column width value + width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto") + + Returns: + True if successful, False otherwise + """ + return set_column_width(table, col_index, width, width_type) + + +def set_column_widths(table, widths, width_type="dxa"): + """ + Set widths for multiple columns in a table. + + Args: + table: The table to modify + widths: List of width values for each column + width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto") + + Returns: + True if successful, False otherwise + """ + try: + for col_index, width in enumerate(widths): + if col_index >= len(table.columns): + break + if not set_column_width(table, col_index, width, width_type): + return False + return True + except Exception as e: + print(f"Error setting column widths: {e}") + return False + + +def set_table_width(table, width, width_type="dxa"): + """ + Set the overall width of a table. + + Args: + table: The table to modify + width: Table width value + width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto") + + Returns: + True if successful, False otherwise + """ + try: + # Convert width based on type + if width_type == "dxa": + # DXA units (twentieths of a point) + if isinstance(width, (int, float)): + width_value = str(int(width * 20)) + else: + width_value = str(width) + elif width_type == "pct": + # Percentage (multiply by 50 for Word format) + if isinstance(width, (int, float)): + width_value = str(int(width * 50)) + else: + width_value = str(width) + else: + width_value = str(width) + + # Get table element and properties + tbl = table._tbl + + # Get or create table properties + tbl_pr = tbl.find(qn('w:tblPr')) + if tbl_pr is None: + tbl_pr = OxmlElement('w:tblPr') + tbl.insert(0, tbl_pr) + + # Remove existing table width + existing_width = tbl_pr.find(qn('w:tblW')) + if existing_width is not None: + tbl_pr.remove(existing_width) + + # Create new table width element + width_element = OxmlElement('w:tblW') + width_element.set(qn('w:w'), width_value) + width_element.set(qn('w:type'), width_type) + + tbl_pr.append(width_element) + + return True + + except Exception as e: + print(f"Error setting table width: {e}") + return False + + +def auto_fit_table(table): + """ + Set table to auto-fit columns based on content. + + Args: + table: The table to modify + + Returns: + True if successful, False otherwise + """ + try: + # Get table element and properties + tbl = table._tbl + + # Get or create table properties + tbl_pr = tbl.find(qn('w:tblPr')) + if tbl_pr is None: + tbl_pr = OxmlElement('w:tblPr') + tbl.insert(0, tbl_pr) + + # Remove existing layout + existing_layout = tbl_pr.find(qn('w:tblLayout')) + if existing_layout is not None: + tbl_pr.remove(existing_layout) + + # Create auto layout element + layout_element = OxmlElement('w:tblLayout') + layout_element.set(qn('w:type'), 'autofit') + + tbl_pr.append(layout_element) + + # Set all column widths to auto + for col_index in range(len(table.columns)): + set_column_width(table, col_index, 0, "auto") + + return True + + except Exception as e: + print(f"Error setting auto-fit table: {e}") + return False + + +def format_cell_text(cell, text_content=None, bold=None, italic=None, underline=None, + color=None, font_size=None, font_name=None): + """ + Format text within a table cell. + + Args: + cell: The table cell to format + text_content: Optional new text content for the cell + bold: Set text bold (True/False) + italic: Set text italic (True/False) + underline: Set text underlined (True/False) + color: Text color (hex string like "FF0000" or color name) + font_size: Font size in points + font_name: Font name/family + + Returns: + True if successful, False otherwise + """ + try: + # Set text content if provided + if text_content is not None: + cell.text = str(text_content) + + # Apply formatting to all paragraphs and runs in the cell + for paragraph in cell.paragraphs: + for run in paragraph.runs: + if bold is not None: + run.bold = bold + if italic is not None: + run.italic = italic + if underline is not None: + run.underline = underline + + if font_size is not None: + from docx.shared import Pt + run.font.size = Pt(font_size) + + if font_name is not None: + run.font.name = font_name + + if color is not None: + from docx.shared import RGBColor + # Define common RGB colors + color_map = { + 'red': RGBColor(255, 0, 0), + 'blue': RGBColor(0, 0, 255), + 'green': RGBColor(0, 128, 0), + 'yellow': RGBColor(255, 255, 0), + 'black': RGBColor(0, 0, 0), + 'gray': RGBColor(128, 128, 128), + 'grey': RGBColor(128, 128, 128), + 'white': RGBColor(255, 255, 255), + 'purple': RGBColor(128, 0, 128), + 'orange': RGBColor(255, 165, 0) + } + + try: + if color.lower() in color_map: + # Use predefined RGB color + run.font.color.rgb = color_map[color.lower()] + elif color.startswith('#'): + # Hex color string + hex_color = color.lstrip('#') + if len(hex_color) == 6: + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + run.font.color.rgb = RGBColor(r, g, b) + else: + # Try hex without # + if len(color) == 6: + r = int(color[0:2], 16) + g = int(color[2:4], 16) + b = int(color[4:6], 16) + run.font.color.rgb = RGBColor(r, g, b) + except Exception: + # If color parsing fails, default to black + run.font.color.rgb = RGBColor(0, 0, 0) + + return True + + except Exception as e: + print(f"Error formatting cell text: {e}") + return False + + +def format_cell_text_by_position(table, row_index, col_index, text_content=None, + bold=None, italic=None, underline=None, color=None, + font_size=None, font_name=None): + """ + Format text in a specific table cell by position. + + Args: + table: The table containing the cell + row_index: Row index (0-based) + col_index: Column index (0-based) + text_content: Optional new text content for the cell + bold: Set text bold (True/False) + italic: Set text italic (True/False) + underline: Set text underlined (True/False) + color: Text color (hex string or color name) + font_size: Font size in points + font_name: Font name/family + + Returns: + True if successful, False otherwise + """ + try: + if (0 <= row_index < len(table.rows) and + 0 <= col_index < len(table.rows[row_index].cells)): + cell = table.rows[row_index].cells[col_index] + return format_cell_text(cell, text_content, bold, italic, underline, + color, font_size, font_name) + else: + return False + except Exception as e: + print(f"Error formatting cell text by position: {e}") + return False + + +def set_cell_padding(cell, top=None, bottom=None, left=None, right=None, unit="dxa"): + """ + Set padding/margins for a table cell. + + Args: + cell: The table cell to format + top: Top padding value + bottom: Bottom padding value + left: Left padding value + right: Right padding value + unit: Unit type ("dxa" for twentieths of a point, "pct" for percentage) + + Returns: + True if successful, False otherwise + """ + try: + # Get or create table cell properties + tc_pr = cell._tc.get_or_add_tcPr() + + # Remove existing margins + existing_margins = tc_pr.find(qn('w:tcMar')) + if existing_margins is not None: + tc_pr.remove(existing_margins) + + # Create margins element if any padding is specified + if any(p is not None for p in [top, bottom, left, right]): + margins_element = OxmlElement('w:tcMar') + + # Add individual margin elements + margin_sides = { + 'w:top': top, + 'w:bottom': bottom, + 'w:left': left, + 'w:right': right + } + + for side, value in margin_sides.items(): + if value is not None: + margin_el = OxmlElement(side) + if unit == "dxa": + # DXA units (twentieths of a point) + margin_el.set(qn('w:w'), str(int(value * 20))) + margin_el.set(qn('w:type'), 'dxa') + elif unit == "pct": + # Percentage + margin_el.set(qn('w:w'), str(int(value * 50))) + margin_el.set(qn('w:type'), 'pct') + else: + # Default to DXA + margin_el.set(qn('w:w'), str(int(value * 20))) + margin_el.set(qn('w:type'), 'dxa') + + margins_element.append(margin_el) + + tc_pr.append(margins_element) + + return True + + except Exception as e: + print(f"Error setting cell padding: {e}") + return False + + +def set_cell_padding_by_position(table, row_index, col_index, top=None, bottom=None, + left=None, right=None, unit="dxa"): + """ + Set padding for a specific table cell by position. + + Args: + table: The table containing the cell + row_index: Row index (0-based) + col_index: Column index (0-based) + top: Top padding value + bottom: Bottom padding value + left: Left padding value + right: Right padding value + unit: Unit type ("dxa" for twentieths of a point, "pct" for percentage) + + Returns: + True if successful, False otherwise + """ + try: + if (0 <= row_index < len(table.rows) and + 0 <= col_index < len(table.rows[row_index].cells)): + cell = table.rows[row_index].cells[col_index] + return set_cell_padding(cell, top, bottom, left, right, unit) + else: + return False + except Exception as e: + print(f"Error setting cell padding by position: {e}") + return False diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/unprotect.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/unprotect.py new file mode 100644 index 00000000..8daddef2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/unprotect.py @@ -0,0 +1,78 @@ +""" +Unprotect document functionality for the Word Document Server. + +This module handles removing document protection. +""" +import os +import json +import hashlib +import tempfile +import shutil +from typing import Tuple, Optional + +def remove_protection_info(filename: str, password: Optional[str] = None) -> Tuple[bool, str]: + """ + Remove protection information from a document and decrypt it if necessary. + + Args: + filename: Path to the Word document + password: Password to verify before removing protection + + Returns: + Tuple of (success, message) + """ + base_path, _ = os.path.splitext(filename) + metadata_path = f"{base_path}.protection" + + # Check if protection metadata exists + if not os.path.exists(metadata_path): + return False, "Document is not protected" + + try: + # Load protection data + with open(metadata_path, 'r') as f: + protection_data = json.load(f) + + # Verify password if provided and required + if password and protection_data.get("password_hash"): + password_hash = hashlib.sha256(password.encode()).hexdigest() + if password_hash != protection_data.get("password_hash"): + return False, "Incorrect password" + + # Handle true encryption if it was applied + if protection_data.get("true_encryption") and password: + try: + import msoffcrypto + + # Create a temporary file for the decrypted output + temp_fd, temp_path = tempfile.mkstemp(suffix='.docx') + os.close(temp_fd) + + # Open the encrypted document + with open(filename, 'rb') as f: + office_file = msoffcrypto.OfficeFile(f) + + # Decrypt with provided password + try: + office_file.load_key(password=password) + + # Write the decrypted file to the temp path + with open(temp_path, 'wb') as out_file: + office_file.decrypt(out_file) + + # Replace encrypted file with decrypted version + shutil.move(temp_path, filename) + except Exception as decrypt_error: + if os.path.exists(temp_path): + os.unlink(temp_path) + return False, f"Failed to decrypt document: {str(decrypt_error)}" + except ImportError: + return False, "Missing msoffcrypto package required for encryption/decryption" + except Exception as e: + return False, f"Error decrypting document: {str(e)}" + + # Remove the protection metadata file + os.remove(metadata_path) + return True, "Protection removed successfully" + except Exception as e: + return False, f"Error removing protection: {str(e)}" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/main.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/main.py new file mode 100644 index 00000000..a4274d73 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/main.py @@ -0,0 +1,766 @@ +""" +Main entry point for the Word Document MCP Server. +Acts as the central controller for the MCP server that handles Word document operations. +Supports multiple transports: stdio, sse, and streamable-http using standalone FastMCP. +""" + +import os +import sys +import builtins +from dotenv import load_dotenv + +# Redirect print to stderr to avoid polluting MCP stdio protocol +_original_print = builtins.print +def _stderr_print(*args, **kwargs): + kwargs.setdefault('file', sys.stderr) + _original_print(*args, **kwargs) +builtins.print = _stderr_print + +# Load environment variables from .env file +print("Loading configuration from .env file...") +load_dotenv() +# Set required environment variable for FastMCP 2.8.1+ +os.environ.setdefault('FASTMCP_LOG_LEVEL', 'INFO') +from fastmcp import FastMCP +from mcp.types import ToolAnnotations +from word_document_server.tools import ( + document_tools, + content_tools, + format_tools, + protection_tools, + footnote_tools, + extended_document_tools, + comment_tools +) +from word_document_server.tools.content_tools import replace_paragraph_block_below_header_tool +from word_document_server.tools.content_tools import replace_block_between_manual_anchors_tool + +def get_transport_config(): + """ + Get transport configuration from environment variables. + + Returns: + dict: Transport configuration with type, host, port, and other settings + """ + # Default configuration + config = { + 'transport': 'stdio', # Default to stdio for backward compatibility + 'host': '0.0.0.0', + 'port': 8000, + 'path': '/mcp', + 'sse_path': '/sse' + } + + # Override with environment variables if provided + transport = os.getenv('MCP_TRANSPORT', 'stdio').lower() + print(f"Transport: {transport}") + # Validate transport type + valid_transports = ['stdio', 'streamable-http', 'sse'] + if transport not in valid_transports: + print(f"Warning: Invalid transport '{transport}'. Falling back to 'stdio'.") + transport = 'stdio' + + config['transport'] = transport + config['host'] = os.getenv('MCP_HOST', config['host']) + # Use PORT from Render if available, otherwise fall back to MCP_PORT or default + config['port'] = int(os.getenv('PORT', os.getenv('MCP_PORT', config['port']))) + config['path'] = os.getenv('MCP_PATH', config['path']) + config['sse_path'] = os.getenv('MCP_SSE_PATH', config['sse_path']) + + return config + + +def setup_logging(debug_mode): + """ + Setup logging based on debug mode. + + Args: + debug_mode (bool): Whether to enable debug logging + """ + import logging + + if debug_mode: + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + print("Debug logging enabled") + else: + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + +# Initialize FastMCP server +mcp = FastMCP("Word Document Server") + + +def register_tools(): + """Register all tools with the MCP server using FastMCP decorators.""" + + # Document tools (create, copy, info, etc.) + @mcp.tool( + annotations=ToolAnnotations( + title="Create Word Document", + destructiveHint=True, + ), + ) + def create_document(filename: str, title: str = None, author: str = None): + """Create a new Word document with optional metadata.""" + return document_tools.create_document(filename, title, author) + + @mcp.tool( + annotations=ToolAnnotations( + title="Copy Word Document", + destructiveHint=True, + ), + ) + def copy_document(source_filename: str, destination_filename: str = None): + """Create a copy of a Word document.""" + return document_tools.copy_document(source_filename, destination_filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Get Document Info", + readOnlyHint=True, + ), + ) + def get_document_info(filename: str): + """Get information about a Word document.""" + return document_tools.get_document_info(filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Get Document Text", + readOnlyHint=True, + ), + ) + def get_document_text(filename: str): + """Extract all text from a Word document.""" + return document_tools.get_document_text(filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Get Document Outline", + readOnlyHint=True, + ), + ) + def get_document_outline(filename: str): + """Get the structure of a Word document.""" + return document_tools.get_document_outline(filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="List Available Documents", + readOnlyHint=True, + ), + ) + def list_available_documents(directory: str = "."): + """List all .docx files in the specified directory.""" + return document_tools.list_available_documents(directory) + + @mcp.tool( + annotations=ToolAnnotations( + title="Get Document XML", + readOnlyHint=True, + ), + ) + def get_document_xml(filename: str): + """Get the raw XML structure of a Word document.""" + return document_tools.get_document_xml_tool(filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Insert Header Near Text", + ), + ) + def insert_header_near_text(filename: str, target_text: str = None, header_title: str = None, position: str = 'after', header_style: str = 'Heading 1', target_paragraph_index: int = None): + """Insert a header (with specified style) before or after the target paragraph. Specify by text or paragraph index. Args: filename (str), target_text (str, optional), header_title (str), position ('before' or 'after'), header_style (str, default 'Heading 1'), target_paragraph_index (int, optional).""" + return content_tools.insert_header_near_text_tool(filename, target_text, header_title, position, header_style, target_paragraph_index) + + @mcp.tool( + annotations=ToolAnnotations( + title="Insert Line Near Text", + ), + ) + def insert_line_or_paragraph_near_text(filename: str, target_text: str = None, line_text: str = None, position: str = 'after', line_style: str = None, target_paragraph_index: int = None): + """ + Insert a new line or paragraph (with specified or matched style) before or after the target paragraph. Specify by text or paragraph index. Args: filename (str), target_text (str, optional), line_text (str), position ('before' or 'after'), line_style (str, optional), target_paragraph_index (int, optional). + """ + return content_tools.insert_line_or_paragraph_near_text_tool(filename, target_text, line_text, position, line_style, target_paragraph_index) + + @mcp.tool( + annotations=ToolAnnotations( + title="Insert List Near Text", + ), + ) + def insert_numbered_list_near_text(filename: str, target_text: str = None, list_items: list[str] = None, position: str = 'after', target_paragraph_index: int = None, bullet_type: str = 'bullet'): + """Insert a bulleted or numbered list before or after the target paragraph. Specify by text or paragraph index. Args: filename (str), target_text (str, optional), list_items (list of str), position ('before' or 'after'), target_paragraph_index (int, optional), bullet_type ('bullet' for bullets or 'number' for numbered lists, default: 'bullet').""" + return content_tools.insert_numbered_list_near_text_tool(filename, target_text, list_items, position, target_paragraph_index, bullet_type) + # Content tools (paragraphs, headings, tables, etc.) + @mcp.tool( + annotations=ToolAnnotations( + title="Add Paragraph", + ), + ) + def add_paragraph(filename: str, text: str, style: str = None, + font_name: str = None, font_size: int = None, + bold: bool = None, italic: bool = None, color: str = None): + """Add a paragraph to a Word document with optional formatting. + + Args: + filename: Path to Word document + text: Paragraph text content + style: Optional paragraph style name + font_name: Font family (e.g., 'Helvetica', 'Times New Roman') + font_size: Font size in points (e.g., 14, 36) + bold: Make text bold + italic: Make text italic + color: Text color as hex RGB (e.g., '000000') + """ + return content_tools.add_paragraph(filename, text, style, font_name, font_size, bold, italic, color) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Heading", + ), + ) + def add_heading(filename: str, text: str, level: int = 1, + font_name: str = None, font_size: int = None, + bold: bool = None, italic: bool = None, border_bottom: bool = False): + """Add a heading to a Word document with optional formatting. + + Args: + filename: Path to Word document + text: Heading text + level: Heading level (1-9) + font_name: Font family (e.g., 'Helvetica') + font_size: Font size in points (e.g., 14) + bold: Make heading bold + italic: Make heading italic + border_bottom: Add bottom border (for section headers) + """ + return content_tools.add_heading(filename, text, level, font_name, font_size, bold, italic, border_bottom) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Picture", + ), + ) + def add_picture(filename: str, image_path: str, width: float = None): + """Add an image to a Word document.""" + return content_tools.add_picture(filename, image_path, width) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Table", + ), + ) + def add_table(filename: str, rows: int, cols: int, data: list[list[str]] = None): + """Add a table to a Word document.""" + return content_tools.add_table(filename, rows, cols, data) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Page Break", + ), + ) + def add_page_break(filename: str): + """Add a page break to the document.""" + return content_tools.add_page_break(filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Delete Paragraph", + destructiveHint=True, + ), + ) + def delete_paragraph(filename: str, paragraph_index: int): + """Delete a paragraph from a document.""" + return content_tools.delete_paragraph(filename, paragraph_index) + + @mcp.tool( + annotations=ToolAnnotations( + title="Search and Replace", + destructiveHint=True, + ), + ) + def search_and_replace(filename: str, find_text: str, replace_text: str): + """Search for text and replace all occurrences.""" + return content_tools.search_and_replace(filename, find_text, replace_text) + + # Format tools (styling, text formatting, etc.) + @mcp.tool( + annotations=ToolAnnotations( + title="Create Custom Style", + ), + ) + def create_custom_style(filename: str, style_name: str, bold: bool = None, + italic: bool = None, font_size: int = None, + font_name: str = None, color: str = None, + base_style: str = None): + """Create a custom style in the document.""" + return format_tools.create_custom_style( + filename, style_name, bold, italic, font_size, font_name, color, base_style + ) + + @mcp.tool( + annotations=ToolAnnotations( + title="Format Text", + ), + ) + def format_text(filename: str, paragraph_index: int, start_pos: int, end_pos: int, + bold: bool = None, italic: bool = None, underline: bool = None, + color: str = None, font_size: int = None, font_name: str = None): + """Format a specific range of text within a paragraph.""" + return format_tools.format_text( + filename, paragraph_index, start_pos, end_pos, bold, italic, + underline, color, font_size, font_name + ) + + @mcp.tool( + annotations=ToolAnnotations( + title="Format Table", + ), + ) + def format_table(filename: str, table_index: int, has_header_row: bool = None, + border_style: str = None, shading: list[str] = None): + """Format a table with borders, shading, and structure.""" + return format_tools.format_table(filename, table_index, has_header_row, border_style, shading) + + # New table cell shading tools + @mcp.tool( + annotations=ToolAnnotations( + title="Set Table Cell Shading", + ), + ) + def set_table_cell_shading(filename: str, table_index: int, row_index: int, + col_index: int, fill_color: str, pattern: str = "clear"): + """Apply shading/filling to a specific table cell.""" + return format_tools.set_table_cell_shading(filename, table_index, row_index, col_index, fill_color, pattern) + + @mcp.tool( + annotations=ToolAnnotations( + title="Apply Alternating Row Colors", + ), + ) + def apply_table_alternating_rows(filename: str, table_index: int, + color1: str = "FFFFFF", color2: str = "F2F2F2"): + """Apply alternating row colors to a table for better readability.""" + return format_tools.apply_table_alternating_rows(filename, table_index, color1, color2) + + @mcp.tool( + annotations=ToolAnnotations( + title="Highlight Table Header", + ), + ) + def highlight_table_header(filename: str, table_index: int, + header_color: str = "4472C4", text_color: str = "FFFFFF"): + """Apply special highlighting to table header row.""" + return format_tools.highlight_table_header(filename, table_index, header_color, text_color) + + # Cell merging tools + @mcp.tool( + annotations=ToolAnnotations( + title="Merge Table Cells", + ), + ) + def merge_table_cells(filename: str, table_index: int, start_row: int, start_col: int, + end_row: int, end_col: int): + """Merge cells in a rectangular area of a table.""" + return format_tools.merge_table_cells(filename, table_index, start_row, start_col, end_row, end_col) + + @mcp.tool( + annotations=ToolAnnotations( + title="Merge Cells Horizontally", + ), + ) + def merge_table_cells_horizontal(filename: str, table_index: int, row_index: int, + start_col: int, end_col: int): + """Merge cells horizontally in a single row.""" + return format_tools.merge_table_cells_horizontal(filename, table_index, row_index, start_col, end_col) + + @mcp.tool( + annotations=ToolAnnotations( + title="Merge Cells Vertically", + ), + ) + def merge_table_cells_vertical(filename: str, table_index: int, col_index: int, + start_row: int, end_row: int): + """Merge cells vertically in a single column.""" + return format_tools.merge_table_cells_vertical(filename, table_index, col_index, start_row, end_row) + + # Cell alignment tools + @mcp.tool( + annotations=ToolAnnotations( + title="Set Cell Alignment", + ), + ) + def set_table_cell_alignment(filename: str, table_index: int, row_index: int, col_index: int, + horizontal: str = "left", vertical: str = "top"): + """Set text alignment for a specific table cell.""" + return format_tools.set_table_cell_alignment(filename, table_index, row_index, col_index, horizontal, vertical) + + @mcp.tool( + annotations=ToolAnnotations( + title="Set Table Alignment", + ), + ) + def set_table_alignment_all(filename: str, table_index: int, + horizontal: str = "left", vertical: str = "top"): + """Set text alignment for all cells in a table.""" + return format_tools.set_table_alignment_all(filename, table_index, horizontal, vertical) + + # Protection tools + @mcp.tool( + annotations=ToolAnnotations( + title="Protect Document", + ), + ) + def protect_document(filename: str, password: str): + """Add password protection to a Word document.""" + return protection_tools.protect_document(filename, password) + + @mcp.tool( + annotations=ToolAnnotations( + title="Unprotect Document", + ), + ) + def unprotect_document(filename: str, password: str): + """Remove password protection from a Word document.""" + return protection_tools.unprotect_document(filename, password) + + # Footnote tools + @mcp.tool( + annotations=ToolAnnotations( + title="Add Footnote", + ), + ) + def add_footnote_to_document(filename: str, paragraph_index: int, footnote_text: str): + """Add a footnote to a specific paragraph in a Word document.""" + return footnote_tools.add_footnote_to_document(filename, paragraph_index, footnote_text) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Footnote After Text", + ), + ) + def add_footnote_after_text(filename: str, search_text: str, footnote_text: str, + output_filename: str = None): + """Add a footnote after specific text with proper superscript formatting. + This enhanced function ensures footnotes display correctly as superscript.""" + return footnote_tools.add_footnote_after_text(filename, search_text, footnote_text, output_filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Footnote Before Text", + ), + ) + def add_footnote_before_text(filename: str, search_text: str, footnote_text: str, + output_filename: str = None): + """Add a footnote before specific text with proper superscript formatting. + This enhanced function ensures footnotes display correctly as superscript.""" + return footnote_tools.add_footnote_before_text(filename, search_text, footnote_text, output_filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Footnote Enhanced", + ), + ) + def add_footnote_enhanced(filename: str, paragraph_index: int, footnote_text: str, + output_filename: str = None): + """Enhanced footnote addition with guaranteed superscript formatting. + Adds footnote at the end of a specific paragraph with proper style handling.""" + return footnote_tools.add_footnote_enhanced(filename, paragraph_index, footnote_text, output_filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Add Endnote", + ), + ) + def add_endnote_to_document(filename: str, paragraph_index: int, endnote_text: str): + """Add an endnote to a specific paragraph in a Word document.""" + return footnote_tools.add_endnote_to_document(filename, paragraph_index, endnote_text) + + @mcp.tool( + annotations=ToolAnnotations( + title="Customize Footnote Style", + ), + ) + def customize_footnote_style(filename: str, numbering_format: str = "1, 2, 3", + start_number: int = 1, font_name: str = None, + font_size: int = None): + """Customize footnote numbering and formatting in a Word document.""" + return footnote_tools.customize_footnote_style( + filename, numbering_format, start_number, font_name, font_size + ) + + @mcp.tool( + annotations=ToolAnnotations( + title="Delete Footnote", + destructiveHint=True, + ), + ) + def delete_footnote_from_document(filename: str, footnote_id: int = None, + search_text: str = None, output_filename: str = None): + """Delete a footnote from a Word document. + Identify the footnote either by ID (1, 2, 3, etc.) or by searching for text near it.""" + return footnote_tools.delete_footnote_from_document( + filename, footnote_id, search_text, output_filename + ) + + # Robust footnote tools - Production-ready with comprehensive validation + @mcp.tool( + annotations=ToolAnnotations( + title="Add Footnote Robust", + ), + ) + def add_footnote_robust(filename: str, search_text: str = None, + paragraph_index: int = None, footnote_text: str = "", + validate_location: bool = True, auto_repair: bool = False): + """Add footnote with robust validation and Word compliance. + This is the production-ready version with comprehensive error handling.""" + return footnote_tools.add_footnote_robust_tool( + filename, search_text, paragraph_index, footnote_text, + validate_location, auto_repair + ) + + @mcp.tool( + annotations=ToolAnnotations( + title="Validate Footnotes", + readOnlyHint=True, + ), + ) + def validate_document_footnotes(filename: str): + """Validate all footnotes in document for coherence and compliance. + Returns detailed report on ID conflicts, orphaned content, missing styles, etc.""" + return footnote_tools.validate_footnotes_tool(filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Delete Footnote Robust", + destructiveHint=True, + ), + ) + def delete_footnote_robust(filename: str, footnote_id: int = None, + search_text: str = None, clean_orphans: bool = True): + """Delete footnote with comprehensive cleanup and orphan removal. + Ensures complete removal from document.xml, footnotes.xml, and relationships.""" + return footnote_tools.delete_footnote_robust_tool( + filename, footnote_id, search_text, clean_orphans + ) + + # Extended document tools + @mcp.tool( + annotations=ToolAnnotations( + title="Get Paragraph Text", + readOnlyHint=True, + ), + ) + def get_paragraph_text_from_document(filename: str, paragraph_index: int): + """Get text from a specific paragraph in a Word document.""" + return extended_document_tools.get_paragraph_text_from_document(filename, paragraph_index) + + @mcp.tool( + annotations=ToolAnnotations( + title="Find Text", + readOnlyHint=True, + ), + ) + def find_text_in_document(filename: str, text_to_find: str, match_case: bool = True, + whole_word: bool = False): + """Find occurrences of specific text in a Word document.""" + return extended_document_tools.find_text_in_document( + filename, text_to_find, match_case, whole_word + ) + + @mcp.tool( + annotations=ToolAnnotations( + title="Convert to PDF", + destructiveHint=True, + ), + ) + def convert_to_pdf(filename: str, output_filename: str = None): + """Convert a Word document to PDF format.""" + return extended_document_tools.convert_to_pdf(filename, output_filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Replace Block Below Header", + ), + ) + def replace_paragraph_block_below_header(filename: str, header_text: str, new_paragraphs: list[str], detect_block_end_fn: str = None): + """Reemplaza el bloque de párrafos debajo de un encabezado, evitando modificar TOC.""" + return replace_paragraph_block_below_header_tool(filename, header_text, new_paragraphs, detect_block_end_fn) + + @mcp.tool( + annotations=ToolAnnotations( + title="Replace Block Between Anchors", + ), + ) + def replace_block_between_manual_anchors(filename: str, start_anchor_text: str, new_paragraphs: list[str], end_anchor_text: str = None, match_fn: str = None, new_paragraph_style: str = None): + """Replace all content between start_anchor_text and end_anchor_text (or next logical header if not provided).""" + return replace_block_between_manual_anchors_tool(filename, start_anchor_text, new_paragraphs, end_anchor_text, match_fn, new_paragraph_style) + + # Comment tools + @mcp.tool( + annotations=ToolAnnotations( + title="Get All Comments", + readOnlyHint=True, + ), + ) + def get_all_comments(filename: str): + """Extract all comments from a Word document.""" + return comment_tools.get_all_comments(filename) + + @mcp.tool( + annotations=ToolAnnotations( + title="Get Comments by Author", + readOnlyHint=True, + ), + ) + def get_comments_by_author(filename: str, author: str): + """Extract comments from a specific author in a Word document.""" + return comment_tools.get_comments_by_author(filename, author) + + @mcp.tool( + annotations=ToolAnnotations( + title="Get Comments for Paragraph", + readOnlyHint=True, + ), + ) + def get_comments_for_paragraph(filename: str, paragraph_index: int): + """Extract comments for a specific paragraph in a Word document.""" + return comment_tools.get_comments_for_paragraph(filename, paragraph_index) + # New table column width tools + @mcp.tool( + annotations=ToolAnnotations( + title="Set Column Width", + ), + ) + def set_table_column_width(filename: str, table_index: int, col_index: int, + width: float, width_type: str = "points"): + """Set the width of a specific table column.""" + return format_tools.set_table_column_width(filename, table_index, col_index, width, width_type) + + @mcp.tool( + annotations=ToolAnnotations( + title="Set Column Widths", + ), + ) + def set_table_column_widths(filename: str, table_index: int, widths: list[float], + width_type: str = "points"): + """Set the widths of multiple table columns.""" + return format_tools.set_table_column_widths(filename, table_index, widths, width_type) + + @mcp.tool( + annotations=ToolAnnotations( + title="Set Table Width", + ), + ) + def set_table_width(filename: str, table_index: int, width: float, + width_type: str = "points"): + """Set the overall width of a table.""" + return format_tools.set_table_width(filename, table_index, width, width_type) + + @mcp.tool( + annotations=ToolAnnotations( + title="Auto-Fit Table Columns", + ), + ) + def auto_fit_table_columns(filename: str, table_index: int): + """Set table columns to auto-fit based on content.""" + return format_tools.auto_fit_table_columns(filename, table_index) + + # New table cell text formatting and padding tools + @mcp.tool( + annotations=ToolAnnotations( + title="Format Cell Text", + ), + ) + def format_table_cell_text(filename: str, table_index: int, row_index: int, col_index: int, + text_content: str = None, bold: bool = None, italic: bool = None, + underline: bool = None, color: str = None, font_size: int = None, + font_name: str = None): + """Format text within a specific table cell.""" + return format_tools.format_table_cell_text(filename, table_index, row_index, col_index, + text_content, bold, italic, underline, color, font_size, font_name) + + @mcp.tool( + annotations=ToolAnnotations( + title="Set Cell Padding", + ), + ) + def set_table_cell_padding(filename: str, table_index: int, row_index: int, col_index: int, + top: float = None, bottom: float = None, left: float = None, + right: float = None, unit: str = "points"): + """Set padding/margins for a specific table cell.""" + return format_tools.set_table_cell_padding(filename, table_index, row_index, col_index, + top, bottom, left, right, unit) + + + +def run_server(): + """Run the Word Document MCP Server with configurable transport.""" + # Get transport configuration + config = get_transport_config() + + # Setup logging + # setup_logging(config['debug']) + + # Register all tools + register_tools() + + # Print startup information + transport_type = config['transport'] + print(f"Starting Word Document MCP Server with {transport_type} transport...") + + # if config['debug']: + # print(f"Configuration: {config}") + + try: + if transport_type == 'stdio': + # Run with stdio transport (default, backward compatible) + print("Server running on stdio transport") + mcp.run(transport='stdio') + + elif transport_type == 'streamable-http': + # Run with streamable HTTP transport + print(f"Server running on streamable-http transport at http://{config['host']}:{config['port']}{config['path']}") + mcp.run( + transport='streamable-http', + host=config['host'], + port=config['port'], + path=config['path'] + ) + + elif transport_type == 'sse': + # Run with SSE transport + print(f"Server running on SSE transport at http://{config['host']}:{config['port']}{config['sse_path']}") + mcp.run( + transport='sse', + host=config['host'], + port=config['port'], + path=config['sse_path'] + ) + + except KeyboardInterrupt: + print("\nShutting down server...") + except Exception as e: + print(f"Error starting server: {e}") + if config['debug']: + import traceback + traceback.print_exc() + sys.exit(1) + + return mcp + + +def main(): + """Main entry point for the server.""" + run_server() + + +if __name__ == "__main__": + main() diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/__init__.py new file mode 100644 index 00000000..e1832536 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/__init__.py @@ -0,0 +1,42 @@ +""" +MCP tool implementations for the Word Document Server. + +This package contains the MCP tool implementations that expose functionality +to clients through the Model Context Protocol. +""" + +# Document tools +from word_document_server.tools.document_tools import ( + create_document, get_document_info, get_document_text, + get_document_outline, list_available_documents, + copy_document, merge_documents +) + +# Content tools +from word_document_server.tools.content_tools import ( + add_heading, add_paragraph, add_table, add_picture, + add_page_break, add_table_of_contents, delete_paragraph, + search_and_replace +) + +# Format tools +from word_document_server.tools.format_tools import ( + format_text, create_custom_style, format_table +) + +# Protection tools +from word_document_server.tools.protection_tools import ( + protect_document, add_restricted_editing, + add_digital_signature, verify_document +) + +# Footnote tools +from word_document_server.tools.footnote_tools import ( + add_footnote_to_document, add_endnote_to_document, + convert_footnotes_to_endnotes_in_document, customize_footnote_style +) + +# Comment tools +from word_document_server.tools.comment_tools import ( + get_all_comments, get_comments_by_author, get_comments_for_paragraph +) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/comment_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/comment_tools.py new file mode 100644 index 00000000..ffe88122 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/comment_tools.py @@ -0,0 +1,168 @@ +""" +Comment extraction tools for Word Document Server. + +These tools provide high-level interfaces for extracting and analyzing +comments from Word documents through the MCP protocol. +""" +import os +import json +from typing import Dict, List, Optional, Any +from docx import Document + +from word_document_server.utils.file_utils import ensure_docx_extension +from word_document_server.core.comments import ( + extract_all_comments, + filter_comments_by_author, + get_comments_for_paragraph +) + + +async def get_all_comments(filename: str) -> str: + """ + Extract all comments from a Word document. + + Args: + filename: Path to the Word document + + Returns: + JSON string containing all comments with metadata + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return json.dumps({ + 'success': False, + 'error': f'Document {filename} does not exist' + }, indent=2) + + try: + # Load the document + doc = Document(filename) + + # Extract all comments + comments = extract_all_comments(doc) + + # Return results + return json.dumps({ + 'success': True, + 'comments': comments, + 'total_comments': len(comments) + }, indent=2) + + except Exception as e: + return json.dumps({ + 'success': False, + 'error': f'Failed to extract comments: {str(e)}' + }, indent=2) + + +async def get_comments_by_author(filename: str, author: str) -> str: + """ + Extract comments from a specific author in a Word document. + + Args: + filename: Path to the Word document + author: Name of the comment author to filter by + + Returns: + JSON string containing filtered comments + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return json.dumps({ + 'success': False, + 'error': f'Document {filename} does not exist' + }, indent=2) + + if not author or not author.strip(): + return json.dumps({ + 'success': False, + 'error': 'Author name cannot be empty' + }, indent=2) + + try: + # Load the document + doc = Document(filename) + + # Extract all comments + all_comments = extract_all_comments(doc) + + # Filter by author + author_comments = filter_comments_by_author(all_comments, author) + + # Return results + return json.dumps({ + 'success': True, + 'author': author, + 'comments': author_comments, + 'total_comments': len(author_comments) + }, indent=2) + + except Exception as e: + return json.dumps({ + 'success': False, + 'error': f'Failed to extract comments: {str(e)}' + }, indent=2) + + +async def get_comments_for_paragraph(filename: str, paragraph_index: int) -> str: + """ + Extract comments for a specific paragraph in a Word document. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph (0-based) + + Returns: + JSON string containing comments for the specified paragraph + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return json.dumps({ + 'success': False, + 'error': f'Document {filename} does not exist' + }, indent=2) + + if paragraph_index < 0: + return json.dumps({ + 'success': False, + 'error': 'Paragraph index must be non-negative' + }, indent=2) + + try: + # Load the document + doc = Document(filename) + + # Check if paragraph index is valid + if paragraph_index >= len(doc.paragraphs): + return json.dumps({ + 'success': False, + 'error': f'Paragraph index {paragraph_index} is out of range. Document has {len(doc.paragraphs)} paragraphs.' + }, indent=2) + + # Extract all comments + all_comments = extract_all_comments(doc) + + # Filter for the specific paragraph + from word_document_server.core.comments import get_comments_for_paragraph as core_get_comments_for_paragraph + para_comments = core_get_comments_for_paragraph(all_comments, paragraph_index) + + # Get the paragraph text for context + paragraph_text = doc.paragraphs[paragraph_index].text + + # Return results + return json.dumps({ + 'success': True, + 'paragraph_index': paragraph_index, + 'paragraph_text': paragraph_text, + 'comments': para_comments, + 'total_comments': len(para_comments) + }, indent=2) + + except Exception as e: + return json.dumps({ + 'success': False, + 'error': f'Failed to extract comments: {str(e)}' + }, indent=2) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/content_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/content_tools.py new file mode 100644 index 00000000..d8250007 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/content_tools.py @@ -0,0 +1,481 @@ +""" +Content tools for Word Document Server. + +These tools add various types of content to Word documents, +including headings, paragraphs, tables, images, and page breaks. +""" +import os +from typing import List, Optional, Dict, Any +from docx import Document +from docx.shared import Inches, Pt, RGBColor + +from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension +from word_document_server.utils.document_utils import find_and_replace_text, insert_header_near_text, insert_numbered_list_near_text, insert_line_or_paragraph_near_text, replace_paragraph_block_below_header, replace_block_between_manual_anchors +from word_document_server.core.styles import ensure_heading_style, ensure_table_style + + +async def add_heading(filename: str, text: str, level: int = 1, + font_name: Optional[str] = None, font_size: Optional[int] = None, + bold: Optional[bool] = None, italic: Optional[bool] = None, + border_bottom: bool = False) -> str: + """Add a heading to a Word document with optional formatting. + + Args: + filename: Path to the Word document + text: Heading text + level: Heading level (1-9, where 1 is the highest level) + font_name: Font family (e.g., 'Helvetica') + font_size: Font size in points (e.g., 14) + bold: True/False for bold text + italic: True/False for italic text + border_bottom: True to add bottom border (for section headers) + """ + filename = ensure_docx_extension(filename) + + # Ensure level is converted to integer + try: + level = int(level) + except (ValueError, TypeError): + return "Invalid parameter: level must be an integer between 1 and 9" + + # Validate level range + if level < 1 or level > 9: + return f"Invalid heading level: {level}. Level must be between 1 and 9." + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + # Suggest creating a copy + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(filename) + + # Ensure heading styles exist + ensure_heading_style(doc) + + # Try to add heading with style + try: + heading = doc.add_heading(text, level=level) + except Exception as style_error: + # If style-based approach fails, use direct formatting + heading = doc.add_paragraph(text) + heading.style = doc.styles['Normal'] + if heading.runs: + run = heading.runs[0] + run.bold = True + # Adjust size based on heading level + if level == 1: + run.font.size = Pt(16) + elif level == 2: + run.font.size = Pt(14) + else: + run.font.size = Pt(12) + + # Apply formatting to all runs in the heading + if any([font_name, font_size, bold is not None, italic is not None]): + for run in heading.runs: + if font_name: + run.font.name = font_name + if font_size: + run.font.size = Pt(font_size) + if bold is not None: + run.font.bold = bold + if italic is not None: + run.font.italic = italic + + # Add bottom border if requested + if border_bottom: + from docx.oxml import OxmlElement + from docx.oxml.ns import qn + + pPr = heading._element.get_or_add_pPr() + pBdr = OxmlElement('w:pBdr') + + bottom = OxmlElement('w:bottom') + bottom.set(qn('w:val'), 'single') + bottom.set(qn('w:sz'), '4') # 0.5pt border + bottom.set(qn('w:space'), '0') + bottom.set(qn('w:color'), '000000') + + pBdr.append(bottom) + pPr.append(pBdr) + + doc.save(filename) + return f"Heading '{text}' (level {level}) added to {filename}" + except Exception as e: + return f"Failed to add heading: {str(e)}" + + +async def add_paragraph(filename: str, text: str, style: Optional[str] = None, + font_name: Optional[str] = None, font_size: Optional[int] = None, + bold: Optional[bool] = None, italic: Optional[bool] = None, + color: Optional[str] = None) -> str: + """Add a paragraph to a Word document with optional formatting. + + Args: + filename: Path to the Word document + text: Paragraph text + style: Optional paragraph style name + font_name: Font family (e.g., 'Helvetica', 'Times New Roman') + font_size: Font size in points (e.g., 14, 36) + bold: True/False for bold text + italic: True/False for italic text + color: RGB color as hex string (e.g., '000000' for black) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + # Suggest creating a copy + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(filename) + paragraph = doc.add_paragraph(text) + + if style: + try: + paragraph.style = style + except KeyError: + # Style doesn't exist, use normal and report it + paragraph.style = doc.styles['Normal'] + doc.save(filename) + return f"Style '{style}' not found, paragraph added with default style to {filename}" + + # Apply formatting to all runs in the paragraph + if any([font_name, font_size, bold is not None, italic is not None, color]): + for run in paragraph.runs: + if font_name: + run.font.name = font_name + if font_size: + run.font.size = Pt(font_size) + if bold is not None: + run.font.bold = bold + if italic is not None: + run.font.italic = italic + if color: + # Remove any '#' prefix if present + color_hex = color.lstrip('#') + run.font.color.rgb = RGBColor.from_string(color_hex) + + doc.save(filename) + return f"Paragraph added to {filename}" + except Exception as e: + return f"Failed to add paragraph: {str(e)}" + + +async def add_table(filename: str, rows: int, cols: int, data: Optional[List[List[str]]] = None) -> str: + """Add a table to a Word document. + + Args: + filename: Path to the Word document + rows: Number of rows in the table + cols: Number of columns in the table + data: Optional 2D array of data to fill the table + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + # Suggest creating a copy + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(filename) + table = doc.add_table(rows=rows, cols=cols) + + # Try to set the table style + try: + table.style = 'Table Grid' + except KeyError: + # If style doesn't exist, add basic borders + pass + + # Fill table with data if provided + if data: + for i, row_data in enumerate(data): + if i >= rows: + break + for j, cell_text in enumerate(row_data): + if j >= cols: + break + table.cell(i, j).text = str(cell_text) + + doc.save(filename) + return f"Table ({rows}x{cols}) added to {filename}" + except Exception as e: + return f"Failed to add table: {str(e)}" + + +async def add_picture(filename: str, image_path: str, width: Optional[float] = None) -> str: + """Add an image to a Word document. + + Args: + filename: Path to the Word document + image_path: Path to the image file + width: Optional width in inches (proportional scaling) + """ + filename = ensure_docx_extension(filename) + + # Validate document existence + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Get absolute paths for better diagnostics + abs_filename = os.path.abspath(filename) + abs_image_path = os.path.abspath(image_path) + + # Validate image existence with improved error message + if not os.path.exists(abs_image_path): + return f"Image file not found: {abs_image_path}" + + # Check image file size + try: + image_size = os.path.getsize(abs_image_path) / 1024 # Size in KB + if image_size <= 0: + return f"Image file appears to be empty: {abs_image_path} (0 KB)" + except Exception as size_error: + return f"Error checking image file: {str(size_error)}" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(abs_filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document." + + try: + doc = Document(abs_filename) + # Additional diagnostic info + diagnostic = f"Attempting to add image ({abs_image_path}, {image_size:.2f} KB) to document ({abs_filename})" + + try: + if width: + doc.add_picture(abs_image_path, width=Inches(width)) + else: + doc.add_picture(abs_image_path) + doc.save(abs_filename) + return f"Picture {image_path} added to {filename}" + except Exception as inner_error: + # More detailed error for the specific operation + error_type = type(inner_error).__name__ + error_msg = str(inner_error) + return f"Failed to add picture: {error_type} - {error_msg or 'No error details available'}\nDiagnostic info: {diagnostic}" + except Exception as outer_error: + # Fallback error handling + error_type = type(outer_error).__name__ + error_msg = str(outer_error) + return f"Document processing error: {error_type} - {error_msg or 'No error details available'}" + + +async def add_page_break(filename: str) -> str: + """Add a page break to the document. + + Args: + filename: Path to the Word document + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + doc.add_page_break() + doc.save(filename) + return f"Page break added to {filename}." + except Exception as e: + return f"Failed to add page break: {str(e)}" + + +async def add_table_of_contents(filename: str, title: str = "Table of Contents", max_level: int = 3) -> str: + """Add a table of contents to a Word document based on heading styles. + + Args: + filename: Path to the Word document + title: Optional title for the table of contents + max_level: Maximum heading level to include (1-9) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + # Ensure max_level is within valid range + max_level = max(1, min(max_level, 9)) + + doc = Document(filename) + + # Collect headings and their positions + headings = [] + for i, paragraph in enumerate(doc.paragraphs): + # Check if paragraph style is a heading + if paragraph.style and paragraph.style.name.startswith('Heading '): + try: + # Extract heading level from style name + level = int(paragraph.style.name.split(' ')[1]) + if level <= max_level: + headings.append({ + 'level': level, + 'text': paragraph.text, + 'position': i + }) + except (ValueError, IndexError): + # Skip if heading level can't be determined + pass + + if not headings: + return f"No headings found in document {filename}. Table of contents not created." + + # Create a new document with the TOC + toc_doc = Document() + + # Add title + if title: + toc_doc.add_heading(title, level=1) + + # Add TOC entries + for heading in headings: + # Indent based on level (using tab characters) + indent = ' ' * (heading['level'] - 1) + toc_doc.add_paragraph(f"{indent}{heading['text']}") + + # Add page break + toc_doc.add_page_break() + + # Get content from original document + for paragraph in doc.paragraphs: + p = toc_doc.add_paragraph(paragraph.text) + # Copy style if possible + try: + if paragraph.style: + p.style = paragraph.style.name + except: + pass + + # Copy tables + for table in doc.tables: + # Create a new table with the same dimensions + new_table = toc_doc.add_table(rows=len(table.rows), cols=len(table.columns)) + # Copy cell contents + for i, row in enumerate(table.rows): + for j, cell in enumerate(row.cells): + for paragraph in cell.paragraphs: + new_table.cell(i, j).text = paragraph.text + + # Save the new document with TOC + toc_doc.save(filename) + + return f"Table of contents with {len(headings)} entries added to {filename}" + except Exception as e: + return f"Failed to add table of contents: {str(e)}" + + +async def delete_paragraph(filename: str, paragraph_index: int) -> str: + """Delete a paragraph from a document. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph to delete (0-based) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate paragraph index + if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs): + return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})." + + # Delete the paragraph (by removing its content and setting it empty) + # Note: python-docx doesn't support true paragraph deletion, this is a workaround + paragraph = doc.paragraphs[paragraph_index] + p = paragraph._p + p.getparent().remove(p) + + doc.save(filename) + return f"Paragraph at index {paragraph_index} deleted successfully." + except Exception as e: + return f"Failed to delete paragraph: {str(e)}" + + +async def search_and_replace(filename: str, find_text: str, replace_text: str) -> str: + """Search for text and replace all occurrences. + + Args: + filename: Path to the Word document + find_text: Text to search for + replace_text: Text to replace with + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Perform find and replace + count = find_and_replace_text(doc, find_text, replace_text) + + if count > 0: + doc.save(filename) + return f"Replaced {count} occurrence(s) of '{find_text}' with '{replace_text}'." + else: + return f"No occurrences of '{find_text}' found." + except Exception as e: + return f"Failed to search and replace: {str(e)}" + +async def insert_header_near_text_tool(filename: str, target_text: str = None, header_title: str = "", position: str = 'after', header_style: str = 'Heading 1', target_paragraph_index: int = None) -> str: + """Insert a header (with specified style) before or after the target paragraph. Specify by text or paragraph index.""" + return insert_header_near_text(filename, target_text, header_title, position, header_style, target_paragraph_index) + +async def insert_numbered_list_near_text_tool(filename: str, target_text: str = None, list_items: list = None, position: str = 'after', target_paragraph_index: int = None, bullet_type: str = 'bullet') -> str: + """Insert a bulleted or numbered list before or after the target paragraph. Specify by text or paragraph index.""" + return insert_numbered_list_near_text(filename, target_text, list_items, position, target_paragraph_index, bullet_type) + +async def insert_line_or_paragraph_near_text_tool(filename: str, target_text: str = None, line_text: str = "", position: str = 'after', line_style: str = None, target_paragraph_index: int = None) -> str: + """Insert a new line or paragraph (with specified or matched style) before or after the target paragraph. Specify by text or paragraph index.""" + return insert_line_or_paragraph_near_text(filename, target_text, line_text, position, line_style, target_paragraph_index) + +async def replace_paragraph_block_below_header_tool(filename: str, header_text: str, new_paragraphs: list, detect_block_end_fn=None) -> str: + """Reemplaza el bloque de párrafos debajo de un encabezado, evitando modificar TOC.""" + return replace_paragraph_block_below_header(filename, header_text, new_paragraphs, detect_block_end_fn) + +async def replace_block_between_manual_anchors_tool(filename: str, start_anchor_text: str, new_paragraphs: list, end_anchor_text: str = None, match_fn=None, new_paragraph_style: str = None) -> str: + """Replace all content between start_anchor_text and end_anchor_text (or next logical header if not provided).""" + return replace_block_between_manual_anchors(filename, start_anchor_text, new_paragraphs, end_anchor_text, match_fn, new_paragraph_style) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/document_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/document_tools.py new file mode 100644 index 00000000..c15ad38d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/document_tools.py @@ -0,0 +1,214 @@ +""" +Document creation and manipulation tools for Word Document Server. +""" +import os +import json +from typing import Dict, List, Optional, Any +from docx import Document + +from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension, create_document_copy +from word_document_server.utils.document_utils import get_document_properties, extract_document_text, get_document_structure, get_document_xml, insert_header_near_text, insert_line_or_paragraph_near_text +from word_document_server.core.styles import ensure_heading_style, ensure_table_style + + +async def create_document(filename: str, title: Optional[str] = None, author: Optional[str] = None) -> str: + """Create a new Word document with optional metadata. + + Args: + filename: Name of the document to create (with or without .docx extension) + title: Optional title for the document metadata + author: Optional author for the document metadata + """ + filename = ensure_docx_extension(filename) + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot create document: {error_message}" + + try: + doc = Document() + + # Set properties if provided + if title: + doc.core_properties.title = title + if author: + doc.core_properties.author = author + + # Ensure necessary styles exist + ensure_heading_style(doc) + ensure_table_style(doc) + + # Save the document + doc.save(filename) + + return f"Document {filename} created successfully" + except Exception as e: + return f"Failed to create document: {str(e)}" + + +async def get_document_info(filename: str) -> str: + """Get information about a Word document. + + Args: + filename: Path to the Word document + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + try: + properties = get_document_properties(filename) + return json.dumps(properties, indent=2) + except Exception as e: + return f"Failed to get document info: {str(e)}" + + +async def get_document_text(filename: str) -> str: + """Extract all text from a Word document. + + Args: + filename: Path to the Word document + """ + filename = ensure_docx_extension(filename) + + return extract_document_text(filename) + + +async def get_document_outline(filename: str) -> str: + """Get the structure of a Word document. + + Args: + filename: Path to the Word document + """ + filename = ensure_docx_extension(filename) + + structure = get_document_structure(filename) + return json.dumps(structure, indent=2) + + +async def list_available_documents(directory: str = ".") -> str: + """List all .docx files in the specified directory. + + Args: + directory: Directory to search for Word documents + """ + try: + if not os.path.exists(directory): + return f"Directory {directory} does not exist" + + docx_files = [f for f in os.listdir(directory) if f.endswith('.docx')] + + if not docx_files: + return f"No Word documents found in {directory}" + + result = f"Found {len(docx_files)} Word documents in {directory}:\n" + for file in docx_files: + file_path = os.path.join(directory, file) + size = os.path.getsize(file_path) / 1024 # KB + result += f"- {file} ({size:.2f} KB)\n" + + return result + except Exception as e: + return f"Failed to list documents: {str(e)}" + + +async def copy_document(source_filename: str, destination_filename: Optional[str] = None) -> str: + """Create a copy of a Word document. + + Args: + source_filename: Path to the source document + destination_filename: Optional path for the copy. If not provided, a default name will be generated. + """ + source_filename = ensure_docx_extension(source_filename) + + if destination_filename: + destination_filename = ensure_docx_extension(destination_filename) + + success, message, new_path = create_document_copy(source_filename, destination_filename) + if success: + return message + else: + return f"Failed to copy document: {message}" + + +async def merge_documents(target_filename: str, source_filenames: List[str], add_page_breaks: bool = True) -> str: + """Merge multiple Word documents into a single document. + + Args: + target_filename: Path to the target document (will be created or overwritten) + source_filenames: List of paths to source documents to merge + add_page_breaks: If True, add page breaks between documents + """ + from word_document_server.core.tables import copy_table + + target_filename = ensure_docx_extension(target_filename) + + # Check if target file is writeable + is_writeable, error_message = check_file_writeable(target_filename) + if not is_writeable: + return f"Cannot create target document: {error_message}" + + # Validate all source documents exist + missing_files = [] + for filename in source_filenames: + doc_filename = ensure_docx_extension(filename) + if not os.path.exists(doc_filename): + missing_files.append(doc_filename) + + if missing_files: + return f"Cannot merge documents. The following source files do not exist: {', '.join(missing_files)}" + + try: + # Create a new document for the merged result + target_doc = Document() + + # Process each source document + for i, filename in enumerate(source_filenames): + doc_filename = ensure_docx_extension(filename) + source_doc = Document(doc_filename) + + # Add page break between documents (except before the first one) + if add_page_breaks and i > 0: + target_doc.add_page_break() + + # Copy all paragraphs + for paragraph in source_doc.paragraphs: + # Create a new paragraph with the same text and style + new_paragraph = target_doc.add_paragraph(paragraph.text) + new_paragraph.style = target_doc.styles['Normal'] # Default style + + # Try to match the style if possible + try: + if paragraph.style and paragraph.style.name in target_doc.styles: + new_paragraph.style = target_doc.styles[paragraph.style.name] + except: + pass + + # Copy run formatting + for i, run in enumerate(paragraph.runs): + if i < len(new_paragraph.runs): + new_run = new_paragraph.runs[i] + # Copy basic formatting + new_run.bold = run.bold + new_run.italic = run.italic + new_run.underline = run.underline + # Font size if specified + if run.font.size: + new_run.font.size = run.font.size + + # Copy all tables + for table in source_doc.tables: + copy_table(table, target_doc) + + # Save the merged document + target_doc.save(target_filename) + return f"Successfully merged {len(source_filenames)} documents into {target_filename}" + except Exception as e: + return f"Failed to merge documents: {str(e)}" + + +async def get_document_xml_tool(filename: str) -> str: + """Get the raw XML structure of a Word document.""" + return get_document_xml(filename) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/extended_document_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/extended_document_tools.py new file mode 100644 index 00000000..8e51b230 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/extended_document_tools.py @@ -0,0 +1,184 @@ +""" +Extended document tools for Word Document Server. + +These tools provide enhanced document content extraction and search capabilities. +""" +import os +import json +import subprocess +import platform +import shutil +from typing import Dict, List, Optional, Any, Union, Tuple +from docx import Document + +from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension +from word_document_server.utils.extended_document_utils import get_paragraph_text, find_text + + +async def get_paragraph_text_from_document(filename: str, paragraph_index: int) -> str: + """Get text from a specific paragraph in a Word document. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph to retrieve (0-based) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + + if paragraph_index < 0: + return "Invalid parameter: paragraph_index must be a non-negative integer" + + try: + result = get_paragraph_text(filename, paragraph_index) + return json.dumps(result, indent=2) + except Exception as e: + return f"Failed to get paragraph text: {str(e)}" + + +async def find_text_in_document(filename: str, text_to_find: str, match_case: bool = True, whole_word: bool = False) -> str: + """Find occurrences of specific text in a Word document. + + Args: + filename: Path to the Word document + text_to_find: Text to search for in the document + match_case: Whether to match case (True) or ignore case (False) + whole_word: Whether to match whole words only (True) or substrings (False) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + if not text_to_find: + return "Search text cannot be empty" + + try: + + result = find_text(filename, text_to_find, match_case, whole_word) + return json.dumps(result, indent=2) + except Exception as e: + return f"Failed to search for text: {str(e)}" + + +async def convert_to_pdf(filename: str, output_filename: Optional[str] = None) -> str: + """Convert a Word document to PDF format. + + Args: + filename: Path to the Word document + output_filename: Optional path for the output PDF. If not provided, + will use the same name with .pdf extension + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Generate output filename if not provided + if not output_filename: + base_name, _ = os.path.splitext(filename) + output_filename = f"{base_name}.pdf" + elif not output_filename.lower().endswith('.pdf'): + output_filename = f"{output_filename}.pdf" + + # Convert to absolute path if not already + if not os.path.isabs(output_filename): + output_filename = os.path.abspath(output_filename) + + # Ensure the output directory exists + output_dir = os.path.dirname(output_filename) + if not output_dir: + output_dir = os.path.abspath('.') + + # Create the directory if it doesn't exist + os.makedirs(output_dir, exist_ok=True) + + # Check if output file can be written + is_writeable, error_message = check_file_writeable(output_filename) + if not is_writeable: + return f"Cannot create PDF: {error_message} (Path: {output_filename}, Dir: {output_dir})" + + try: + # Determine platform for appropriate conversion method + system = platform.system() + + if system == "Windows": + # On Windows, try docx2pdf which uses Microsoft Word + try: + from docx2pdf import convert + convert(filename, output_filename) + return f"Document successfully converted to PDF: {output_filename}" + except (ImportError, Exception) as e: + return f"Failed to convert document to PDF: {str(e)}\nNote: docx2pdf requires Microsoft Word to be installed." + + elif system in ["Linux", "Darwin"]: # Linux or macOS + errors = [] + + # --- Attempt 1: LibreOffice --- + lo_commands = [] + if system == "Darwin": # macOS + lo_commands = ["soffice", "/Applications/LibreOffice.app/Contents/MacOS/soffice"] + else: # Linux + lo_commands = ["libreoffice", "soffice"] + + for cmd_name in lo_commands: + try: + output_dir_for_lo = os.path.dirname(output_filename) or '.' + os.makedirs(output_dir_for_lo, exist_ok=True) + + cmd = [cmd_name, '--headless', '--convert-to', 'pdf', '--outdir', output_dir_for_lo, filename] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, check=False) + + if result.returncode == 0: + # LibreOffice typically creates a PDF with the same base name as the source file. + # e.g., 'mydoc.docx' -> 'mydoc.pdf' + base_name = os.path.splitext(os.path.basename(filename))[0] + created_pdf_name = f"{base_name}.pdf" + created_pdf_path = os.path.join(output_dir_for_lo, created_pdf_name) + + # If the created file exists, move it to the desired output_filename if necessary. + if os.path.exists(created_pdf_path): + if created_pdf_path != output_filename: + shutil.move(created_pdf_path, output_filename) + + # Final check: does the target file now exist? + if os.path.exists(output_filename): + return f"Document successfully converted to PDF via {cmd_name}: {output_filename}" + + # If we get here, soffice returned 0 but the expected file wasn't created. + errors.append(f"{cmd_name} returned success code, but output file '{created_pdf_path}' was not found.") + # Continue to the next command or fallback. + else: + errors.append(f"{cmd_name} failed. Stderr: {result.stderr.strip()}") + except FileNotFoundError: + errors.append(f"Command '{cmd_name}' not found.") + except (subprocess.SubprocessError, Exception) as e: + errors.append(f"An error occurred with {cmd_name}: {str(e)}") + + # --- Attempt 2: docx2pdf (Fallback) --- + try: + from docx2pdf import convert + convert(filename, output_filename) + if os.path.exists(output_filename) and os.path.getsize(output_filename) > 0: + return f"Document successfully converted to PDF via docx2pdf: {output_filename}" + else: + errors.append("docx2pdf fallback was executed but failed to create a valid output file.") + except ImportError: + errors.append("docx2pdf is not installed, skipping fallback.") + except Exception as e: + errors.append(f"docx2pdf fallback failed with an exception: {str(e)}") + + # --- If all attempts failed --- + error_summary = "Failed to convert document to PDF using all available methods.\n" + error_summary += "Recorded errors: " + "; ".join(errors) + "\n" + error_summary += "To convert documents to PDF, please install either:\n" + error_summary += "1. LibreOffice (recommended for Linux/macOS)\n" + error_summary += "2. Microsoft Word (required for docx2pdf on Windows/macOS)" + return error_summary + else: + return f"PDF conversion not supported on {system} platform" + + except Exception as e: + return f"Failed to convert document to PDF: {str(e)}" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/footnote_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/footnote_tools.py new file mode 100644 index 00000000..72b0190d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/footnote_tools.py @@ -0,0 +1,709 @@ +""" +Footnote and endnote tools for Word Document Server. + +These tools handle footnote and endnote functionality, +including adding, customizing, and converting between them. + +This module combines both standard and robust implementations: +- String-return functions for backward compatibility +- Dict-return robust functions for structured responses +""" +import os +from typing import Optional, Dict, Any +from docx import Document +from docx.shared import Pt +from docx.enum.style import WD_STYLE_TYPE + +from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension +from word_document_server.core.footnotes import ( + find_footnote_references, + get_format_symbols, + customize_footnote_formatting, + add_footnote_robust, + delete_footnote_robust, + validate_document_footnotes, + add_footnote_at_paragraph_end # Compatibility function +) + + +async def add_footnote_to_document(filename: str, paragraph_index: int, footnote_text: str) -> str: + """Add a footnote to a specific paragraph in a Word document. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph to add footnote to (0-based) + footnote_text: Text content of the footnote + """ + filename = ensure_docx_extension(filename) + + # Ensure paragraph_index is an integer + try: + paragraph_index = int(paragraph_index) + except (ValueError, TypeError): + return "Invalid parameter: paragraph_index must be an integer" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate paragraph index + if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs): + return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})." + + paragraph = doc.paragraphs[paragraph_index] + + # In python-docx, we'd use paragraph.add_footnote(), but we'll use a more robust approach + try: + footnote = paragraph.add_run() + footnote.text = "" + + # Create the footnote reference + reference = footnote.add_footnote(footnote_text) + + doc.save(filename) + return f"Footnote added to paragraph {paragraph_index} in {filename}" + except AttributeError: + # Fall back to a simpler approach if direct footnote addition fails + last_run = paragraph.add_run() + last_run.text = "¹" # Unicode superscript 1 + last_run.font.superscript = True + + # Add a footnote section at the end if it doesn't exist + found_footnote_section = False + for p in doc.paragraphs: + if p.text.startswith("Footnotes:"): + found_footnote_section = True + break + + if not found_footnote_section: + doc.add_paragraph("\n").add_run() + doc.add_paragraph("Footnotes:").bold = True + + # Add footnote text + footnote_para = doc.add_paragraph("¹ " + footnote_text) + footnote_para.style = "Footnote Text" if "Footnote Text" in doc.styles else "Normal" + + doc.save(filename) + return f"Footnote added to paragraph {paragraph_index} in {filename} (simplified approach)" + except Exception as e: + return f"Failed to add footnote: {str(e)}" + + +async def add_endnote_to_document(filename: str, paragraph_index: int, endnote_text: str) -> str: + """Add an endnote to a specific paragraph in a Word document. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph to add endnote to (0-based) + endnote_text: Text content of the endnote + """ + filename = ensure_docx_extension(filename) + + # Ensure paragraph_index is an integer + try: + paragraph_index = int(paragraph_index) + except (ValueError, TypeError): + return "Invalid parameter: paragraph_index must be an integer" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate paragraph index + if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs): + return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})." + + paragraph = doc.paragraphs[paragraph_index] + + # Add endnote reference + last_run = paragraph.add_run() + last_run.text = "†" # Unicode dagger symbol common for endnotes + last_run.font.superscript = True + + # Check if endnotes section exists, if not create it + endnotes_heading_found = False + for para in doc.paragraphs: + if para.text == "Endnotes:" or para.text == "ENDNOTES": + endnotes_heading_found = True + break + + if not endnotes_heading_found: + # Add a page break before endnotes section + doc.add_page_break() + doc.add_heading("Endnotes:", level=1) + + # Add the endnote text + endnote_para = doc.add_paragraph("† " + endnote_text) + endnote_para.style = "Endnote Text" if "Endnote Text" in doc.styles else "Normal" + + doc.save(filename) + return f"Endnote added to paragraph {paragraph_index} in {filename}" + except Exception as e: + return f"Failed to add endnote: {str(e)}" + + +async def convert_footnotes_to_endnotes_in_document(filename: str) -> str: + """Convert all footnotes to endnotes in a Word document. + + Args: + filename: Path to the Word document + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + + # Find all runs that might be footnote references + footnote_references = [] + + for para_idx, para in enumerate(doc.paragraphs): + for run_idx, run in enumerate(para.runs): + # Check if this run is likely a footnote reference + # (superscript number or special character) + if run.font.superscript and (run.text.isdigit() or run.text in "¹²³⁴⁵⁶⁷⁸⁹"): + footnote_references.append({ + "paragraph_index": para_idx, + "run_index": run_idx, + "text": run.text + }) + + if not footnote_references: + return f"No footnote references found in {filename}" + + # Create endnotes section + doc.add_page_break() + doc.add_heading("Endnotes:", level=1) + + # Create a placeholder for endnote content, we'll fill it later + endnote_content = [] + + # Find the footnote text at the bottom of the page + + found_footnote_section = False + footnote_text = [] + + for para in doc.paragraphs: + if not found_footnote_section and para.text.startswith("Footnotes:"): + found_footnote_section = True + continue + + if found_footnote_section: + footnote_text.append(para.text) + + # Create endnotes based on footnote references + for i, ref in enumerate(footnote_references): + # Add a new endnote + endnote_para = doc.add_paragraph() + + # Try to match with footnote text, or use placeholder + if i < len(footnote_text): + endnote_para.text = f"†{i+1} {footnote_text[i]}" + else: + endnote_para.text = f"†{i+1} Converted from footnote {ref['text']}" + + # Change the footnote reference to an endnote reference + try: + paragraph = doc.paragraphs[ref["paragraph_index"]] + paragraph.runs[ref["run_index"]].text = f"†{i+1}" + except IndexError: + # Skip if we can't locate the reference + pass + + # Save the document + doc.save(filename) + + return f"Converted {len(footnote_references)} footnotes to endnotes in {filename}" + except Exception as e: + return f"Failed to convert footnotes to endnotes: {str(e)}" + + +async def add_footnote_after_text(filename: str, search_text: str, footnote_text: str, + output_filename: Optional[str] = None) -> str: + """Add a footnote after specific text in a Word document with proper formatting. + + This enhanced function ensures proper superscript formatting by managing styles at the XML level. + + Args: + filename: Path to the Word document + search_text: Text to search for (footnote will be added after this text) + footnote_text: Content of the footnote + output_filename: Optional output filename (if None, modifies in place) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + # Use robust implementation + success, message, details = add_footnote_robust( + filename=filename, + search_text=search_text, + footnote_text=footnote_text, + output_filename=output_filename, + position="after", + validate_location=True + ) + return message + except Exception as e: + return f"Failed to add footnote: {str(e)}" + + +async def add_footnote_before_text(filename: str, search_text: str, footnote_text: str, + output_filename: Optional[str] = None) -> str: + """Add a footnote before specific text in a Word document with proper formatting. + + This enhanced function ensures proper superscript formatting by managing styles at the XML level. + + Args: + filename: Path to the Word document + search_text: Text to search for (footnote will be added before this text) + footnote_text: Content of the footnote + output_filename: Optional output filename (if None, modifies in place) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + # Use robust implementation + success, message, details = add_footnote_robust( + filename=filename, + search_text=search_text, + footnote_text=footnote_text, + output_filename=output_filename, + position="before", + validate_location=True + ) + return message + except Exception as e: + return f"Failed to add footnote: {str(e)}" + + +async def add_footnote_enhanced(filename: str, paragraph_index: int, footnote_text: str, + output_filename: Optional[str] = None) -> str: + """Enhanced version of add_footnote_to_document with proper superscript formatting. + + Now uses the robust implementation for better reliability. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph to add footnote to (0-based) + footnote_text: Text content of the footnote + output_filename: Optional output filename (if None, modifies in place) + """ + filename = ensure_docx_extension(filename) + + # Ensure paragraph_index is an integer + try: + paragraph_index = int(paragraph_index) + except (ValueError, TypeError): + return "Invalid parameter: paragraph_index must be an integer" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + # Use robust implementation + success, message, details = add_footnote_robust( + filename=filename, + paragraph_index=paragraph_index, + footnote_text=footnote_text, + output_filename=output_filename, + validate_location=True + ) + return message + except Exception as e: + return f"Failed to add footnote: {str(e)}" + + +async def customize_footnote_style(filename: str, numbering_format: str = "1, 2, 3", + start_number: int = 1, font_name: Optional[str] = None, + font_size: Optional[int] = None) -> str: + """Customize footnote numbering and formatting in a Word document. + + Args: + filename: Path to the Word document + numbering_format: Format for footnote numbers (e.g., "1, 2, 3", "i, ii, iii", "a, b, c") + start_number: Number to start footnote numbering from + font_name: Optional font name for footnotes + font_size: Optional font size for footnotes (in points) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Create or get footnote style + footnote_style_name = "Footnote Text" + footnote_style = None + + try: + footnote_style = doc.styles[footnote_style_name] + except KeyError: + # Create the style if it doesn't exist + footnote_style = doc.styles.add_style(footnote_style_name, WD_STYLE_TYPE.PARAGRAPH) + + # Apply formatting to footnote style + if footnote_style: + if font_name: + footnote_style.font.name = font_name + if font_size: + footnote_style.font.size = Pt(font_size) + + # Find all existing footnote references + footnote_refs = find_footnote_references(doc) + + # Generate format symbols for the specified numbering format + format_symbols = get_format_symbols(numbering_format, len(footnote_refs) + start_number) + + # Apply custom formatting to footnotes + count = customize_footnote_formatting(doc, footnote_refs, format_symbols, start_number, footnote_style) + + # Save the document + doc.save(filename) + + return f"Footnote style and numbering customized in {filename}" + except Exception as e: + return f"Failed to customize footnote style: {str(e)}" + + +async def delete_footnote_from_document(filename: str, footnote_id: Optional[int] = None, + search_text: Optional[str] = None, + output_filename: Optional[str] = None) -> str: + """Delete a footnote from a Word document. + + You can identify the footnote to delete either by: + 1. footnote_id: The numeric ID of the footnote (1, 2, 3, etc.) + 2. search_text: Text near the footnote reference to find and delete + + Args: + filename: Path to the Word document + footnote_id: Optional ID of the footnote to delete (1-based) + search_text: Optional text to search near the footnote reference + output_filename: Optional output filename (if None, modifies in place) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + # Use robust implementation with orphan cleanup + success, message, details = delete_footnote_robust( + filename=filename, + footnote_id=footnote_id, + search_text=search_text, + output_filename=output_filename, + clean_orphans=True + ) + return message + except Exception as e: + return f"Failed to delete footnote: {str(e)}" + + +# ============================================================================ +# Robust tool functions with Dict returns for structured responses +# ============================================================================ + + +async def add_footnote_robust_tool( + filename: str, + search_text: Optional[str] = None, + paragraph_index: Optional[int] = None, + footnote_text: str = "", + validate_location: bool = True, + auto_repair: bool = False +) -> Dict[str, Any]: + """ + Add a footnote with robust validation and error handling. + + This is the production-ready version with comprehensive Word compliance. + + Args: + filename: Path to the Word document + search_text: Text to search for (mutually exclusive with paragraph_index) + paragraph_index: Index of paragraph (mutually exclusive with search_text) + footnote_text: Content of the footnote + validate_location: Whether to validate placement restrictions + auto_repair: Whether to attempt automatic document repair + + Returns: + Dict with success status, message, and optional details + """ + filename = ensure_docx_extension(filename) + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return { + "success": False, + "message": f"Cannot modify document: {error_message}", + "details": None + } + + # Convert paragraph_index if provided as string + if paragraph_index is not None: + try: + paragraph_index = int(paragraph_index) + except (ValueError, TypeError): + return { + "success": False, + "message": "Invalid parameter: paragraph_index must be an integer", + "details": None + } + + # Call robust implementation + success, message, details = add_footnote_robust( + filename=filename, + search_text=search_text, + paragraph_index=paragraph_index, + footnote_text=footnote_text, + validate_location=validate_location, + auto_repair=auto_repair + ) + + return { + "success": success, + "message": message, + "details": details + } + + +async def delete_footnote_robust_tool( + filename: str, + footnote_id: Optional[int] = None, + search_text: Optional[str] = None, + clean_orphans: bool = True +) -> Dict[str, Any]: + """ + Delete a footnote with comprehensive cleanup. + + Args: + filename: Path to the Word document + footnote_id: ID of footnote to delete + search_text: Text near footnote reference + clean_orphans: Whether to remove orphaned content + + Returns: + Dict with success status, message, and optional details + """ + filename = ensure_docx_extension(filename) + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return { + "success": False, + "message": f"Cannot modify document: {error_message}", + "details": None + } + + # Convert footnote_id if provided as string + if footnote_id is not None: + try: + footnote_id = int(footnote_id) + except (ValueError, TypeError): + return { + "success": False, + "message": "Invalid parameter: footnote_id must be an integer", + "details": None + } + + # Call robust implementation + success, message, details = delete_footnote_robust( + filename=filename, + footnote_id=footnote_id, + search_text=search_text, + clean_orphans=clean_orphans + ) + + return { + "success": success, + "message": message, + "details": details + } + + +async def validate_footnotes_tool(filename: str) -> Dict[str, Any]: + """ + Validate all footnotes in a document. + + Provides comprehensive validation report including: + - ID conflicts + - Orphaned content + - Missing styles + - Invalid locations + - Coherence issues + + Args: + filename: Path to the Word document + + Returns: + Dict with validation status and detailed report + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return { + "valid": False, + "message": f"Document {filename} does not exist", + "report": {} + } + + # Call validation + is_valid, message, report = validate_document_footnotes(filename) + + return { + "valid": is_valid, + "message": message, + "report": report + } + + +# ============================================================================ +# Compatibility wrappers for robust tools (maintain backward compatibility) +# ============================================================================ + +async def add_footnote_to_document_robust( + filename: str, + paragraph_index: int, + footnote_text: str +) -> str: + """ + Robust version of add_footnote_to_document. + Maintains backward compatibility with existing API. + """ + result = await add_footnote_robust_tool( + filename=filename, + paragraph_index=paragraph_index, + footnote_text=footnote_text + ) + return result["message"] + + +async def add_footnote_after_text_robust( + filename: str, + search_text: str, + footnote_text: str, + output_filename: Optional[str] = None +) -> str: + """ + Robust version of add_footnote_after_text. + Maintains backward compatibility with existing API. + """ + # Handle output filename by copying first if needed + working_file = filename + if output_filename: + import shutil + shutil.copy2(filename, output_filename) + working_file = output_filename + + result = await add_footnote_robust_tool( + filename=working_file, + search_text=search_text, + footnote_text=footnote_text + ) + return result["message"] + + +async def add_footnote_before_text_robust( + filename: str, + search_text: str, + footnote_text: str, + output_filename: Optional[str] = None +) -> str: + """ + Robust version of add_footnote_before_text. + Note: Current robust implementation defaults to 'after' position. + """ + # Handle output filename + working_file = filename + if output_filename: + import shutil + shutil.copy2(filename, output_filename) + working_file = output_filename + + result = await add_footnote_robust_tool( + filename=working_file, + search_text=search_text, + footnote_text=footnote_text + ) + return result["message"] + + +async def delete_footnote_from_document_robust( + filename: str, + footnote_id: Optional[int] = None, + search_text: Optional[str] = None, + output_filename: Optional[str] = None +) -> str: + """ + Robust version of delete_footnote_from_document. + Maintains backward compatibility with existing API. + """ + # Handle output filename + working_file = filename + if output_filename: + import shutil + shutil.copy2(filename, output_filename) + working_file = output_filename + + result = await delete_footnote_robust_tool( + filename=working_file, + footnote_id=footnote_id, + search_text=search_text + ) + return result["message"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/format_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/format_tools.py new file mode 100644 index 00000000..a60fc0c4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/format_tools.py @@ -0,0 +1,1112 @@ +""" +Formatting tools for Word Document Server. + +These tools handle formatting operations for Word documents, +including text formatting, table formatting, and custom styles. +""" +import os +from typing import List, Optional, Dict, Any +from docx import Document +from docx.shared import Pt, RGBColor +from docx.enum.text import WD_COLOR_INDEX +from docx.enum.style import WD_STYLE_TYPE + +from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension +from word_document_server.core.styles import create_style +from word_document_server.core.tables import ( + apply_table_style, set_cell_shading_by_position, apply_alternating_row_shading, + highlight_header_row, merge_cells, merge_cells_horizontal, merge_cells_vertical, + set_cell_alignment_by_position, set_table_alignment, set_column_width_by_position, + set_column_widths, set_table_width as set_table_width_func, auto_fit_table, + format_cell_text_by_position, set_cell_padding_by_position +) + + +async def format_text(filename: str, paragraph_index: int, start_pos: int, end_pos: int, + bold: Optional[bool] = None, italic: Optional[bool] = None, + underline: Optional[bool] = None, color: Optional[str] = None, + font_size: Optional[int] = None, font_name: Optional[str] = None) -> str: + """Format a specific range of text within a paragraph. + + Args: + filename: Path to the Word document + paragraph_index: Index of the paragraph (0-based) + start_pos: Start position within the paragraph text + end_pos: End position within the paragraph text + bold: Set text bold (True/False) + italic: Set text italic (True/False) + underline: Set text underlined (True/False) + color: Text color (e.g., 'red', 'blue', etc.) + font_size: Font size in points + font_name: Font name/family + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + paragraph_index = int(paragraph_index) + start_pos = int(start_pos) + end_pos = int(end_pos) + if font_size is not None: + font_size = int(font_size) + except (ValueError, TypeError): + return "Invalid parameter: paragraph_index, start_pos, end_pos, and font_size must be integers" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate paragraph index + if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs): + return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})." + + paragraph = doc.paragraphs[paragraph_index] + text = paragraph.text + + # Validate text positions + if start_pos < 0 or end_pos > len(text) or start_pos >= end_pos: + return f"Invalid text positions. Paragraph has {len(text)} characters." + + # Get the text to format + target_text = text[start_pos:end_pos] + + # Clear existing runs and create three runs: before, target, after + for run in paragraph.runs: + run.clear() + + # Add text before target + if start_pos > 0: + run_before = paragraph.add_run(text[:start_pos]) + + # Add target text with formatting + run_target = paragraph.add_run(target_text) + if bold is not None: + run_target.bold = bold + if italic is not None: + run_target.italic = italic + if underline is not None: + run_target.underline = underline + if color: + # Define common RGB colors + color_map = { + 'red': RGBColor(255, 0, 0), + 'blue': RGBColor(0, 0, 255), + 'green': RGBColor(0, 128, 0), + 'yellow': RGBColor(255, 255, 0), + 'black': RGBColor(0, 0, 0), + 'gray': RGBColor(128, 128, 128), + 'white': RGBColor(255, 255, 255), + 'purple': RGBColor(128, 0, 128), + 'orange': RGBColor(255, 165, 0) + } + + try: + if color.lower() in color_map: + # Use predefined RGB color + run_target.font.color.rgb = color_map[color.lower()] + else: + # Try to set color by name + run_target.font.color.rgb = RGBColor.from_string(color) + except Exception as e: + # If all else fails, default to black + run_target.font.color.rgb = RGBColor(0, 0, 0) + if font_size: + run_target.font.size = Pt(font_size) + if font_name: + run_target.font.name = font_name + + # Add text after target + if end_pos < len(text): + run_after = paragraph.add_run(text[end_pos:]) + + doc.save(filename) + return f"Text '{target_text}' formatted successfully in paragraph {paragraph_index}." + except Exception as e: + return f"Failed to format text: {str(e)}" + + +async def create_custom_style(filename: str, style_name: str, + bold: Optional[bool] = None, italic: Optional[bool] = None, + font_size: Optional[int] = None, font_name: Optional[str] = None, + color: Optional[str] = None, base_style: Optional[str] = None) -> str: + """Create a custom style in the document. + + Args: + filename: Path to the Word document + style_name: Name for the new style + bold: Set text bold (True/False) + italic: Set text italic (True/False) + font_size: Font size in points + font_name: Font name/family + color: Text color (e.g., 'red', 'blue') + base_style: Optional existing style to base this on + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Build font properties dictionary + font_properties = {} + if bold is not None: + font_properties['bold'] = bold + if italic is not None: + font_properties['italic'] = italic + if font_size is not None: + font_properties['size'] = font_size + if font_name is not None: + font_properties['name'] = font_name + if color is not None: + font_properties['color'] = color + + # Create the style + new_style = create_style( + doc, + style_name, + WD_STYLE_TYPE.PARAGRAPH, + base_style=base_style, + font_properties=font_properties + ) + + doc.save(filename) + return f"Style '{style_name}' created successfully." + except Exception as e: + return f"Failed to create style: {str(e)}" + + +async def format_table(filename: str, table_index: int, + has_header_row: Optional[bool] = None, + border_style: Optional[str] = None, + shading: Optional[List[List[str]]] = None) -> str: + """Format a table with borders, shading, and structure. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + has_header_row: If True, formats the first row as a header + border_style: Style for borders ('none', 'single', 'double', 'thick') + shading: 2D list of cell background colors (by row and column) + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply formatting + success = apply_table_style(table, has_header_row or False, border_style, shading) + + if success: + doc.save(filename) + return f"Table at index {table_index} formatted successfully." + else: + return f"Failed to format table at index {table_index}." + except Exception as e: + return f"Failed to format table: {str(e)}" + + +async def set_table_cell_shading(filename: str, table_index: int, row_index: int, + col_index: int, fill_color: str, pattern: str = "clear") -> str: + """Apply shading/filling to a specific table cell. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + row_index: Row index of the cell (0-based) + col_index: Column index of the cell (0-based) + fill_color: Background color (hex string like "FF0000" or "red") + pattern: Shading pattern ("clear", "solid", "pct10", "pct20", etc.) + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + row_index = int(row_index) + col_index = int(col_index) + except (ValueError, TypeError): + return "Invalid parameter: table_index, row_index, and col_index must be integers" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Validate row and column indices + if row_index < 0 or row_index >= len(table.rows): + return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})." + + if col_index < 0 or col_index >= len(table.rows[row_index].cells): + return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})." + + # Apply cell shading + success = set_cell_shading_by_position(table, row_index, col_index, fill_color, pattern) + + if success: + doc.save(filename) + return f"Cell shading applied successfully to table {table_index}, row {row_index}, column {col_index}." + else: + return f"Failed to apply cell shading." + except Exception as e: + return f"Failed to apply cell shading: {str(e)}" + + +async def apply_table_alternating_rows(filename: str, table_index: int, + color1: str = "FFFFFF", color2: str = "F2F2F2") -> str: + """Apply alternating row colors to a table for better readability. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + color1: Color for odd rows (hex string, default white) + color2: Color for even rows (hex string, default light gray) + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + except (ValueError, TypeError): + return "Invalid parameter: table_index must be an integer" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply alternating row shading + success = apply_alternating_row_shading(table, color1, color2) + + if success: + doc.save(filename) + return f"Alternating row shading applied successfully to table {table_index}." + else: + return f"Failed to apply alternating row shading." + except Exception as e: + return f"Failed to apply alternating row shading: {str(e)}" + + +async def highlight_table_header(filename: str, table_index: int, + header_color: str = "4472C4", text_color: str = "FFFFFF") -> str: + """Apply special highlighting to table header row. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + header_color: Background color for header (hex string, default blue) + text_color: Text color for header (hex string, default white) + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + except (ValueError, TypeError): + return "Invalid parameter: table_index must be an integer" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply header highlighting + success = highlight_header_row(table, header_color, text_color) + + if success: + doc.save(filename) + return f"Header highlighting applied successfully to table {table_index}." + else: + return f"Failed to apply header highlighting." + except Exception as e: + return f"Failed to apply header highlighting: {str(e)}" + + +async def merge_table_cells(filename: str, table_index: int, start_row: int, start_col: int, + end_row: int, end_col: int) -> str: + """Merge cells in a rectangular area of a table. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + start_row: Starting row index (0-based) + start_col: Starting column index (0-based) + end_row: Ending row index (0-based, inclusive) + end_col: Ending column index (0-based, inclusive) + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + start_row = int(start_row) + start_col = int(start_col) + end_row = int(end_row) + end_col = int(end_col) + except (ValueError, TypeError): + return "Invalid parameter: all indices must be integers" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Validate merge parameters + if start_row > end_row or start_col > end_col: + return "Invalid merge range: start indices must be <= end indices" + + if start_row == end_row and start_col == end_col: + return "Invalid merge range: cannot merge a single cell with itself" + + # Apply cell merge + success = merge_cells(table, start_row, start_col, end_row, end_col) + + if success: + doc.save(filename) + return f"Cells merged successfully in table {table_index} from ({start_row},{start_col}) to ({end_row},{end_col})." + else: + return f"Failed to merge cells. Check that indices are valid." + except Exception as e: + return f"Failed to merge cells: {str(e)}" + + +async def merge_table_cells_horizontal(filename: str, table_index: int, row_index: int, + start_col: int, end_col: int) -> str: + """Merge cells horizontally in a single row. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + row_index: Row index (0-based) + start_col: Starting column index (0-based) + end_col: Ending column index (0-based, inclusive) + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + row_index = int(row_index) + start_col = int(start_col) + end_col = int(end_col) + except (ValueError, TypeError): + return "Invalid parameter: all indices must be integers" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply horizontal cell merge + success = merge_cells_horizontal(table, row_index, start_col, end_col) + + if success: + doc.save(filename) + return f"Cells merged horizontally in table {table_index}, row {row_index}, columns {start_col}-{end_col}." + else: + return f"Failed to merge cells horizontally. Check that indices are valid." + except Exception as e: + return f"Failed to merge cells horizontally: {str(e)}" + + +async def merge_table_cells_vertical(filename: str, table_index: int, col_index: int, + start_row: int, end_row: int) -> str: + """Merge cells vertically in a single column. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + col_index: Column index (0-based) + start_row: Starting row index (0-based) + end_row: Ending row index (0-based, inclusive) + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + col_index = int(col_index) + start_row = int(start_row) + end_row = int(end_row) + except (ValueError, TypeError): + return "Invalid parameter: all indices must be integers" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply vertical cell merge + success = merge_cells_vertical(table, col_index, start_row, end_row) + + if success: + doc.save(filename) + return f"Cells merged vertically in table {table_index}, column {col_index}, rows {start_row}-{end_row}." + else: + return f"Failed to merge cells vertically. Check that indices are valid." + except Exception as e: + return f"Failed to merge cells vertically: {str(e)}" + + +async def set_table_cell_alignment(filename: str, table_index: int, row_index: int, col_index: int, + horizontal: str = "left", vertical: str = "top") -> str: + """Set text alignment for a specific table cell. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + row_index: Row index (0-based) + col_index: Column index (0-based) + horizontal: Horizontal alignment ("left", "center", "right", "justify") + vertical: Vertical alignment ("top", "center", "bottom") + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + row_index = int(row_index) + col_index = int(col_index) + except (ValueError, TypeError): + return "Invalid parameter: table_index, row_index, and col_index must be integers" + + # Validate alignment parameters + valid_horizontal = ["left", "center", "right", "justify"] + valid_vertical = ["top", "center", "bottom"] + + if horizontal.lower() not in valid_horizontal: + return f"Invalid horizontal alignment. Valid options: {', '.join(valid_horizontal)}" + + if vertical.lower() not in valid_vertical: + return f"Invalid vertical alignment. Valid options: {', '.join(valid_vertical)}" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply cell alignment + success = set_cell_alignment_by_position(table, row_index, col_index, horizontal, vertical) + + if success: + doc.save(filename) + return f"Cell alignment set successfully for table {table_index}, cell ({row_index},{col_index}) to {horizontal}/{vertical}." + else: + return f"Failed to set cell alignment. Check that indices are valid." + except Exception as e: + return f"Failed to set cell alignment: {str(e)}" + + +async def set_table_alignment_all(filename: str, table_index: int, + horizontal: str = "left", vertical: str = "top") -> str: + """Set text alignment for all cells in a table. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + horizontal: Horizontal alignment ("left", "center", "right", "justify") + vertical: Vertical alignment ("top", "center", "bottom") + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + except (ValueError, TypeError): + return "Invalid parameter: table_index must be an integer" + + # Validate alignment parameters + valid_horizontal = ["left", "center", "right", "justify"] + valid_vertical = ["top", "center", "bottom"] + + if horizontal.lower() not in valid_horizontal: + return f"Invalid horizontal alignment. Valid options: {', '.join(valid_horizontal)}" + + if vertical.lower() not in valid_vertical: + return f"Invalid vertical alignment. Valid options: {', '.join(valid_vertical)}" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply table alignment + success = set_table_alignment(table, horizontal, vertical) + + if success: + doc.save(filename) + return f"Table alignment set successfully for table {table_index} to {horizontal}/{vertical} for all cells." + else: + return f"Failed to set table alignment." + except Exception as e: + return f"Failed to set table alignment: {str(e)}" + + +async def set_table_column_width(filename: str, table_index: int, col_index: int, + width: float, width_type: str = "points") -> str: + """Set the width of a specific table column. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + col_index: Column index (0-based) + width: Column width value + width_type: Width type ("points", "inches", "cm", "percent", "auto") + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + col_index = int(col_index) + if width_type != "auto": + width = float(width) + except (ValueError, TypeError): + return "Invalid parameter: table_index and col_index must be integers, width must be a number" + + # Validate width type + valid_width_types = ["points", "inches", "cm", "percent", "auto"] + if width_type.lower() not in valid_width_types: + return f"Invalid width type. Valid options: {', '.join(valid_width_types)}" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Validate column index + if col_index < 0 or col_index >= len(table.columns): + return f"Invalid column index. Table has {len(table.columns)} columns (0-{len(table.columns)-1})." + + # Convert width and type for Word format + if width_type.lower() == "points": + # Points to DXA (twentieths of a point) + word_width = width + word_type = "dxa" + elif width_type.lower() == "inches": + # Inches to points, then to DXA + word_width = width * 72 # 72 points per inch + word_type = "dxa" + elif width_type.lower() == "cm": + # CM to points, then to DXA + word_width = width * 28.35 # ~28.35 points per cm + word_type = "dxa" + elif width_type.lower() == "percent": + # Percentage (Word uses 50x the percentage value) + word_width = width + word_type = "pct" + else: # auto + word_width = 0 + word_type = "auto" + + # Apply column width + success = set_column_width_by_position(table, col_index, word_width, word_type) + + if success: + doc.save(filename) + return f"Column width set successfully for table {table_index}, column {col_index} to {width} {width_type}." + else: + return f"Failed to set column width. Check that indices are valid." + except Exception as e: + return f"Failed to set column width: {str(e)}" + + +async def set_table_column_widths(filename: str, table_index: int, widths: list, + width_type: str = "points") -> str: + """Set the widths of multiple table columns. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + widths: List of width values for each column + width_type: Width type ("points", "inches", "cm", "percent", "auto") + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + if width_type != "auto": + widths = [float(w) for w in widths] + except (ValueError, TypeError): + return "Invalid parameter: table_index must be an integer, widths must be a list of numbers" + + # Validate width type + valid_width_types = ["points", "inches", "cm", "percent", "auto"] + if width_type.lower() not in valid_width_types: + return f"Invalid width type. Valid options: {', '.join(valid_width_types)}" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Convert widths and type for Word format + word_widths = [] + for width in widths: + if width_type.lower() == "points": + word_widths.append(width) + elif width_type.lower() == "inches": + word_widths.append(width * 72) # 72 points per inch + elif width_type.lower() == "cm": + word_widths.append(width * 28.35) # ~28.35 points per cm + elif width_type.lower() == "percent": + word_widths.append(width) + else: # auto + word_widths.append(0) + + # Determine Word type + if width_type.lower() == "percent": + word_type = "pct" + elif width_type.lower() == "auto": + word_type = "auto" + else: + word_type = "dxa" + + # Apply column widths + success = set_column_widths(table, word_widths, word_type) + + if success: + doc.save(filename) + return f"Column widths set successfully for table {table_index} with {len(widths)} columns in {width_type}." + else: + return f"Failed to set column widths." + except Exception as e: + return f"Failed to set column widths: {str(e)}" + + +async def set_table_width(filename: str, table_index: int, width: float, + width_type: str = "points") -> str: + """Set the overall width of a table. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + width: Table width value + width_type: Width type ("points", "inches", "cm", "percent", "auto") + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + if width_type != "auto": + width = float(width) + except (ValueError, TypeError): + return "Invalid parameter: table_index must be an integer, width must be a number" + + # Validate width type + valid_width_types = ["points", "inches", "cm", "percent", "auto"] + if width_type.lower() not in valid_width_types: + return f"Invalid width type. Valid options: {', '.join(valid_width_types)}" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Convert width and type for Word format + if width_type.lower() == "points": + word_width = width + word_type = "dxa" + elif width_type.lower() == "inches": + word_width = width * 72 # 72 points per inch + word_type = "dxa" + elif width_type.lower() == "cm": + word_width = width * 28.35 # ~28.35 points per cm + word_type = "dxa" + elif width_type.lower() == "percent": + word_width = width + word_type = "pct" + else: # auto + word_width = 0 + word_type = "auto" + + # Apply table width + success = set_table_width_func(table, word_width, word_type) + + if success: + doc.save(filename) + return f"Table width set successfully for table {table_index} to {width} {width_type}." + else: + return f"Failed to set table width." + except Exception as e: + return f"Failed to set table width: {str(e)}" + + +async def auto_fit_table_columns(filename: str, table_index: int) -> str: + """Set table columns to auto-fit based on content. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + except (ValueError, TypeError): + return "Invalid parameter: table_index must be an integer" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Apply auto-fit + success = auto_fit_table(table) + + if success: + doc.save(filename) + return f"Table {table_index} set to auto-fit columns based on content." + else: + return f"Failed to set table auto-fit." + except Exception as e: + return f"Failed to set table auto-fit: {str(e)}" + + +async def format_table_cell_text(filename: str, table_index: int, row_index: int, col_index: int, + text_content: Optional[str] = None, bold: Optional[bool] = None, italic: Optional[bool] = None, + underline: Optional[bool] = None, color: Optional[str] = None, font_size: Optional[int] = None, + font_name: Optional[str] = None) -> str: + """Format text within a specific table cell. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + row_index: Row index (0-based) + col_index: Column index (0-based) + text_content: Optional new text content for the cell + bold: Set text bold (True/False) + italic: Set text italic (True/False) + underline: Set text underlined (True/False) + color: Text color (hex string like "FF0000" or color name like "red") + font_size: Font size in points + font_name: Font name/family + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + row_index = int(row_index) + col_index = int(col_index) + if font_size is not None: + font_size = int(font_size) + except (ValueError, TypeError): + return "Invalid parameter: table_index, row_index, col_index must be integers, font_size must be integer" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Validate row and column indices + if row_index < 0 or row_index >= len(table.rows): + return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})." + + if col_index < 0 or col_index >= len(table.rows[row_index].cells): + return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})." + + # Apply cell text formatting + success = format_cell_text_by_position(table, row_index, col_index, text_content, + bold, italic, underline, color, font_size, font_name) + + if success: + doc.save(filename) + format_desc = [] + if text_content is not None: + format_desc.append(f"content='{text_content[:30]}{'...' if len(text_content) > 30 else ''}'") + if bold is not None: + format_desc.append(f"bold={bold}") + if italic is not None: + format_desc.append(f"italic={italic}") + if underline is not None: + format_desc.append(f"underline={underline}") + if color is not None: + format_desc.append(f"color={color}") + if font_size is not None: + format_desc.append(f"size={font_size}pt") + if font_name is not None: + format_desc.append(f"font={font_name}") + + format_str = ", ".join(format_desc) if format_desc else "no changes" + return f"Cell text formatted successfully in table {table_index}, cell ({row_index},{col_index}): {format_str}." + else: + return f"Failed to format cell text. Check that indices are valid." + except Exception as e: + return f"Failed to format cell text: {str(e)}" + + +async def set_table_cell_padding(filename: str, table_index: int, row_index: int, col_index: int, + top: Optional[float] = None, bottom: Optional[float] = None, left: Optional[float] = None, + right: Optional[float] = None, unit: str = "points") -> str: + """Set padding/margins for a specific table cell. + + Args: + filename: Path to the Word document + table_index: Index of the table (0-based) + row_index: Row index (0-based) + col_index: Column index (0-based) + top: Top padding in specified units + bottom: Bottom padding in specified units + left: Left padding in specified units + right: Right padding in specified units + unit: Unit type ("points" or "percent") + """ + filename = ensure_docx_extension(filename) + + # Ensure numeric parameters are the correct type + try: + table_index = int(table_index) + row_index = int(row_index) + col_index = int(col_index) + if top is not None: + top = float(top) + if bottom is not None: + bottom = float(bottom) + if left is not None: + left = float(left) + if right is not None: + right = float(right) + except (ValueError, TypeError): + return "Invalid parameter: indices must be integers, padding values must be numbers" + + # Validate unit + valid_units = ["points", "percent"] + if unit.lower() not in valid_units: + return f"Invalid unit. Valid options: {', '.join(valid_units)}" + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}. Consider creating a copy first." + + try: + doc = Document(filename) + + # Validate table index + if table_index < 0 or table_index >= len(doc.tables): + return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})." + + table = doc.tables[table_index] + + # Validate row and column indices + if row_index < 0 or row_index >= len(table.rows): + return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})." + + if col_index < 0 or col_index >= len(table.rows[row_index].cells): + return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})." + + # Convert unit for Word format + word_unit = "dxa" if unit.lower() == "points" else "pct" + + # Apply cell padding + success = set_cell_padding_by_position(table, row_index, col_index, top, bottom, + left, right, word_unit) + + if success: + doc.save(filename) + padding_desc = [] + if top is not None: + padding_desc.append(f"top={top}") + if bottom is not None: + padding_desc.append(f"bottom={bottom}") + if left is not None: + padding_desc.append(f"left={left}") + if right is not None: + padding_desc.append(f"right={right}") + + padding_str = ", ".join(padding_desc) if padding_desc else "no padding" + return f"Cell padding set successfully for table {table_index}, cell ({row_index},{col_index}): {padding_str} {unit}." + else: + return f"Failed to set cell padding. Check that indices are valid." + except Exception as e: + return f"Failed to set cell padding: {str(e)}" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/protection_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/protection_tools.py new file mode 100644 index 00000000..e52fd34a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/protection_tools.py @@ -0,0 +1,275 @@ +""" +Protection tools for Word Document Server. + +These tools handle document protection features such as +password protection, restricted editing, and digital signatures. +""" +import os +import hashlib +import datetime +import io +from typing import List, Optional, Dict, Any +from docx import Document +import msoffcrypto + +from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension + + + +from word_document_server.core.protection import ( + add_protection_info, + verify_document_protection, + create_signature_info +) + + +async def protect_document(filename: str, password: str) -> str: + """Add password protection to a Word document. + + Args: + filename: Path to the Word document + password: Password to protect the document with + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot protect document: {error_message}" + + try: + # Read the original file content + with open(filename, "rb") as infile: + original_data = infile.read() + + # Create an msoffcrypto file object from the original data + file = msoffcrypto.OfficeFile(io.BytesIO(original_data)) + file.load_key(password=password) # Set the password for encryption + + # Encrypt the data into an in-memory buffer + encrypted_data_io = io.BytesIO() + + file.encrypt(password=password, outfile=encrypted_data_io) + + # Overwrite the original file with the encrypted data + with open(filename, "wb") as outfile: + outfile.write(encrypted_data_io.getvalue()) + + + base_path, _ = os.path.splitext(filename) + metadata_path = f"{base_path}.protection" + if os.path.exists(metadata_path): + os.remove(metadata_path) + + return f"Document {filename} encrypted successfully with password." + + except Exception as e: + # Attempt to restore original file content on failure + try: + if 'original_data' in locals(): + with open(filename, "wb") as outfile: + outfile.write(original_data) + return f"Failed to encrypt document {filename}: {str(e)}. Original file restored." + else: + return f"Failed to encrypt document {filename}: {str(e)}. Could not restore original file." + except Exception as restore_e: + return f"Failed to encrypt document {filename}: {str(e)}. Also failed to restore original file: {str(restore_e)}" + + +async def add_restricted_editing(filename: str, password: str, editable_sections: List[str]) -> str: + """Add restricted editing to a Word document, allowing editing only in specified sections. + + Args: + filename: Path to the Word document + password: Password to protect the document with + editable_sections: List of section names that can be edited + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot protect document: {error_message}" + + try: + # Hash the password for security + password_hash = hashlib.sha256(password.encode()).hexdigest() + + # Add protection info to metadata + success = add_protection_info( + filename, + protection_type="restricted", + password_hash=password_hash, + sections=editable_sections + ) + + if not editable_sections: + return "No editable sections specified. Document will be fully protected." + + if success: + return f"Document {filename} protected with restricted editing. Editable sections: {', '.join(editable_sections)}" + else: + return f"Failed to protect document {filename} with restricted editing" + except Exception as e: + return f"Failed to add restricted editing: {str(e)}" + +async def add_digital_signature(filename: str, signer_name: str, reason: Optional[str] = None) -> str: + """Add a digital signature to a Word document. + + Args: + filename: Path to the Word document + signer_name: Name of the person signing the document + reason: Optional reason for signing + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot add signature to document: {error_message}" + + try: + doc = Document(filename) + + # Create signature info + signature_info = create_signature_info(doc, signer_name, reason) + + # Add protection info to metadata + success = add_protection_info( + filename, + protection_type="signature", + password_hash="", # No password for signature-only + signature_info=signature_info + ) + + if success: + # Add a visible signature block to the document + doc.add_paragraph("").add_run() # Add empty paragraph for spacing + signature_para = doc.add_paragraph() + signature_para.add_run(f"Digitally signed by: {signer_name}").bold = True + if reason: + signature_para.add_run(f"\nReason: {reason}") + signature_para.add_run(f"\nDate: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + signature_para.add_run(f"\nSignature ID: {signature_info['content_hash'][:8]}") + + # Save the document with the visible signature + doc.save(filename) + + return f"Digital signature added to document {filename}" + else: + return f"Failed to add digital signature to document {filename}" + except Exception as e: + return f"Failed to add digital signature: {str(e)}" + +async def verify_document(filename: str, password: Optional[str] = None) -> str: + """Verify document protection and/or digital signature. + + Args: + filename: Path to the Word document + password: Optional password to verify + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + try: + # Verify document protection + is_verified, message = verify_document_protection(filename, password) + + if not is_verified and password: + return f"Document verification failed: {message}" + + # If document has a digital signature, verify content integrity + base_path, _ = os.path.splitext(filename) + metadata_path = f"{base_path}.protection" + + if os.path.exists(metadata_path): + try: + import json + with open(metadata_path, 'r') as f: + protection_data = json.load(f) + + if protection_data.get("type") == "signature": + # Get the original content hash + signature_info = protection_data.get("signature", {}) + original_hash = signature_info.get("content_hash") + + if original_hash: + # Calculate current content hash + doc = Document(filename) + text_content = "\n".join([p.text for p in doc.paragraphs]) + current_hash = hashlib.sha256(text_content.encode()).hexdigest() + + # Compare hashes + if current_hash != original_hash: + return f"Document has been modified since it was signed by {signature_info.get('signer')}" + else: + return f"Document signature is valid. Signed by {signature_info.get('signer')} on {signature_info.get('timestamp')}" + except Exception as e: + return f"Error verifying signature: {str(e)}" + + return message + except Exception as e: + return f"Failed to verify document: {str(e)}" + +async def unprotect_document(filename: str, password: str) -> str: + """Remove password protection from a Word document. + + Args: + filename: Path to the Word document + password: Password that was used to protect the document + """ + filename = ensure_docx_extension(filename) + + if not os.path.exists(filename): + return f"Document {filename} does not exist" + + # Check if file is writeable + is_writeable, error_message = check_file_writeable(filename) + if not is_writeable: + return f"Cannot modify document: {error_message}" + + try: + # Read the encrypted file content + with open(filename, "rb") as infile: + encrypted_data = infile.read() + + # Create an msoffcrypto file object from the encrypted data + file = msoffcrypto.OfficeFile(io.BytesIO(encrypted_data)) + file.load_key(password=password) # Set the password for decryption + + # Decrypt the data into an in-memory buffer + decrypted_data_io = io.BytesIO() + file.decrypt(outfile=decrypted_data_io) # Pass the buffer as the 'outfile' argument + + # Overwrite the original file with the decrypted data + with open(filename, "wb") as outfile: + outfile.write(decrypted_data_io.getvalue()) + + return f"Document {filename} decrypted successfully." + + except msoffcrypto.exceptions.InvalidKeyError: + return f"Failed to decrypt document {filename}: Incorrect password." + except msoffcrypto.exceptions.InvalidFormatError: + return f"Failed to decrypt document {filename}: File is not encrypted or is not a supported Office format." + except Exception as e: + # Attempt to restore encrypted file content on failure + try: + if 'encrypted_data' in locals(): + with open(filename, "wb") as outfile: + outfile.write(encrypted_data) + return f"Failed to decrypt document {filename}: {str(e)}. Encrypted file restored." + else: + return f"Failed to decrypt document {filename}: {str(e)}. Could not restore encrypted file." + except Exception as restore_e: + return f"Failed to decrypt document {filename}: {str(e)}. Also failed to restore encrypted file: {str(restore_e)}" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/__init__.py new file mode 100644 index 00000000..1146a586 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/__init__.py @@ -0,0 +1,8 @@ +""" +Utility functions for the Word Document Server. + +This package contains utility modules for file operations and document handling. +""" + +from word_document_server.utils.file_utils import check_file_writeable, create_document_copy, ensure_docx_extension +from word_document_server.utils.document_utils import get_document_properties, extract_document_text, get_document_structure, find_paragraph_by_text, find_and_replace_text diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/document_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/document_utils.py new file mode 100644 index 00000000..c3681c08 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/document_utils.py @@ -0,0 +1,618 @@ +""" +Document utility functions for Word Document Server. +""" +import json +from typing import Dict, List, Any +from docx import Document +from docx.oxml.table import CT_Tbl +from docx.oxml.text.paragraph import CT_P +from docx.oxml.ns import qn +from docx.oxml import OxmlElement + + +def get_document_properties(doc_path: str) -> Dict[str, Any]: + """Get properties of a Word document.""" + import os + if not os.path.exists(doc_path): + return {"error": f"Document {doc_path} does not exist"} + + try: + doc = Document(doc_path) + core_props = doc.core_properties + + return { + "title": core_props.title or "", + "author": core_props.author or "", + "subject": core_props.subject or "", + "keywords": core_props.keywords or "", + "created": str(core_props.created) if core_props.created else "", + "modified": str(core_props.modified) if core_props.modified else "", + "last_modified_by": core_props.last_modified_by or "", + "revision": core_props.revision or 0, + "page_count": len(doc.sections), + "word_count": sum(len(paragraph.text.split()) for paragraph in doc.paragraphs), + "paragraph_count": len(doc.paragraphs), + "table_count": len(doc.tables) + } + except Exception as e: + return {"error": f"Failed to get document properties: {str(e)}"} + + +def extract_document_text(doc_path: str) -> str: + """Extract all text from a Word document.""" + import os + if not os.path.exists(doc_path): + return f"Document {doc_path} does not exist" + + try: + doc = Document(doc_path) + text = [] + + for paragraph in doc.paragraphs: + text.append(paragraph.text) + + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + text.append(paragraph.text) + + return "\n".join(text) + except Exception as e: + return f"Failed to extract text: {str(e)}" + + +def get_document_structure(doc_path: str) -> Dict[str, Any]: + """Get the structure of a Word document.""" + import os + if not os.path.exists(doc_path): + return {"error": f"Document {doc_path} does not exist"} + + try: + doc = Document(doc_path) + structure = { + "paragraphs": [], + "tables": [] + } + + # Get paragraphs + for i, para in enumerate(doc.paragraphs): + structure["paragraphs"].append({ + "index": i, + "text": para.text[:100] + ("..." if len(para.text) > 100 else ""), + "style": para.style.name if para.style else "Normal" + }) + + # Get tables + for i, table in enumerate(doc.tables): + table_data = { + "index": i, + "rows": len(table.rows), + "columns": len(table.columns), + "preview": [] + } + + # Get sample of table data + max_rows = min(3, len(table.rows)) + for row_idx in range(max_rows): + row_data = [] + max_cols = min(3, len(table.columns)) + for col_idx in range(max_cols): + try: + cell_text = table.cell(row_idx, col_idx).text + row_data.append(cell_text[:20] + ("..." if len(cell_text) > 20 else "")) + except IndexError: + row_data.append("N/A") + table_data["preview"].append(row_data) + + structure["tables"].append(table_data) + + return structure + except Exception as e: + return {"error": f"Failed to get document structure: {str(e)}"} + + +def find_paragraph_by_text(doc, text, partial_match=False): + """ + Find paragraphs containing specific text. + + Args: + doc: Document object + text: Text to search for + partial_match: If True, matches paragraphs containing the text; if False, matches exact text + + Returns: + List of paragraph indices that match the criteria + """ + matching_paragraphs = [] + + for i, para in enumerate(doc.paragraphs): + if partial_match and text in para.text: + matching_paragraphs.append(i) + elif not partial_match and para.text == text: + matching_paragraphs.append(i) + + return matching_paragraphs + + +def find_and_replace_text(doc, old_text, new_text): + """ + Find and replace text throughout the document, skipping Table of Contents (TOC) paragraphs. + + Args: + doc: Document object + old_text: Text to find + new_text: Text to replace with + + Returns: + Number of replacements made + """ + count = 0 + + # Search in paragraphs + for para in doc.paragraphs: + # Skip TOC paragraphs + if para.style and para.style.name.startswith("TOC"): + continue + if old_text in para.text: + for run in para.runs: + if old_text in run.text: + run.text = run.text.replace(old_text, new_text) + count += 1 + + # Search in tables + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + # Skip TOC paragraphs in tables + if para.style and para.style.name.startswith("TOC"): + continue + if old_text in para.text: + for run in para.runs: + if old_text in run.text: + run.text = run.text.replace(old_text, new_text) + count += 1 + + return count + + +def get_document_xml(doc_path: str) -> str: + """Extract and return the raw XML structure of the Word document (word/document.xml).""" + import os + import zipfile + if not os.path.exists(doc_path): + return f"Document {doc_path} does not exist" + try: + with zipfile.ZipFile(doc_path) as docx_zip: + with docx_zip.open('word/document.xml') as xml_file: + return xml_file.read().decode('utf-8') + except Exception as e: + return f"Failed to extract XML: {str(e)}" + + +def insert_header_near_text(doc_path: str, target_text: str = None, header_title: str = "", position: str = 'after', header_style: str = 'Heading 1', target_paragraph_index: int = None) -> str: + """Insert a header (with specified style) before or after the target paragraph. Specify by text or paragraph index. Skips TOC paragraphs in text search.""" + import os + from docx import Document + if not os.path.exists(doc_path): + return f"Document {doc_path} does not exist" + try: + doc = Document(doc_path) + found = False + para = None + if target_paragraph_index is not None: + if target_paragraph_index < 0 or target_paragraph_index >= len(doc.paragraphs): + return f"Invalid target_paragraph_index: {target_paragraph_index}. Document has {len(doc.paragraphs)} paragraphs." + para = doc.paragraphs[target_paragraph_index] + found = True + else: + for i, p in enumerate(doc.paragraphs): + # Skip TOC paragraphs + if p.style and p.style.name.lower().startswith("toc"): + continue + if target_text and target_text in p.text: + para = p + found = True + break + if not found or para is None: + return f"Target paragraph not found (by index or text). (TOC paragraphs are skipped in text search)" + # Save anchor index before insertion + if target_paragraph_index is not None: + anchor_index = target_paragraph_index + else: + anchor_index = None + for i, p in enumerate(doc.paragraphs): + if p is para: + anchor_index = i + break + new_para = doc.add_paragraph(header_title, style=header_style) + if position == 'before': + para._element.addprevious(new_para._element) + else: + para._element.addnext(new_para._element) + doc.save(doc_path) + if anchor_index is not None: + return f"Header '{header_title}' (style: {header_style}) inserted {position} paragraph (index {anchor_index})." + else: + return f"Header '{header_title}' (style: {header_style}) inserted {position} the target paragraph." + except Exception as e: + return f"Failed to insert header: {str(e)}" + + +def insert_line_or_paragraph_near_text(doc_path: str, target_text: str = None, line_text: str = "", position: str = 'after', line_style: str = None, target_paragraph_index: int = None) -> str: + """ + Insert a new line or paragraph (with specified or matched style) before or after the target paragraph. + You can specify the target by text (first match) or by paragraph index. + Skips paragraphs whose style name starts with 'TOC' if using text search. + """ + import os + from docx import Document + if not os.path.exists(doc_path): + return f"Document {doc_path} does not exist" + try: + doc = Document(doc_path) + found = False + para = None + if target_paragraph_index is not None: + if target_paragraph_index < 0 or target_paragraph_index >= len(doc.paragraphs): + return f"Invalid target_paragraph_index: {target_paragraph_index}. Document has {len(doc.paragraphs)} paragraphs." + para = doc.paragraphs[target_paragraph_index] + found = True + else: + for i, p in enumerate(doc.paragraphs): + # Skip TOC paragraphs + if p.style and p.style.name.lower().startswith("toc"): + continue + if target_text and target_text in p.text: + para = p + found = True + break + if not found or para is None: + return f"Target paragraph not found (by index or text). (TOC paragraphs are skipped in text search)" + # Save anchor index before insertion + if target_paragraph_index is not None: + anchor_index = target_paragraph_index + else: + anchor_index = None + for i, p in enumerate(doc.paragraphs): + if p is para: + anchor_index = i + break + # Determine style: use provided or match target + style = line_style if line_style else para.style + new_para = doc.add_paragraph(line_text, style=style) + if position == 'before': + para._element.addprevious(new_para._element) + else: + para._element.addnext(new_para._element) + doc.save(doc_path) + if anchor_index is not None: + return f"Line/paragraph inserted {position} paragraph (index {anchor_index}) with style '{style}'." + else: + return f"Line/paragraph inserted {position} the target paragraph with style '{style}'." + except Exception as e: + return f"Failed to insert line/paragraph: {str(e)}" + + +def add_bullet_numbering(paragraph, num_id=1, level=0): + """ + Add bullet/numbering XML to a paragraph. + + Args: + paragraph: python-docx Paragraph object + num_id: Numbering definition ID (1=bullets, 2=numbers, etc.) + level: Indentation level (0=first level, 1=second level, etc.) + + Returns: + The modified paragraph + """ + # Get or create paragraph properties + pPr = paragraph._element.get_or_add_pPr() + + # Remove existing numPr if any (to avoid duplicates) + existing_numPr = pPr.find(qn('w:numPr')) + if existing_numPr is not None: + pPr.remove(existing_numPr) + + # Create numbering properties element + numPr = OxmlElement('w:numPr') + + # Set indentation level + ilvl = OxmlElement('w:ilvl') + ilvl.set(qn('w:val'), str(level)) + numPr.append(ilvl) + + # Set numbering definition ID + numId = OxmlElement('w:numId') + numId.set(qn('w:val'), str(num_id)) + numPr.append(numId) + + # Add to paragraph properties + pPr.append(numPr) + + return paragraph + + +def insert_numbered_list_near_text(doc_path: str, target_text: str = None, list_items: list = None, position: str = 'after', target_paragraph_index: int = None, bullet_type: str = 'bullet') -> str: + """ + Insert a bulleted or numbered list before or after the target paragraph. Specify by text or paragraph index. Skips TOC paragraphs in text search. + Args: + doc_path: Path to the Word document + target_text: Text to search for in paragraphs (optional if using index) + list_items: List of strings, each as a list item + position: 'before' or 'after' (default: 'after') + target_paragraph_index: Optional paragraph index to use as anchor + bullet_type: 'bullet' for bullets (•), 'number' for numbers (1,2,3) (default: 'bullet') + Returns: + Status message + """ + import os + from docx import Document + if not os.path.exists(doc_path): + return f"Document {doc_path} does not exist" + try: + doc = Document(doc_path) + found = False + para = None + if target_paragraph_index is not None: + if target_paragraph_index < 0 or target_paragraph_index >= len(doc.paragraphs): + return f"Invalid target_paragraph_index: {target_paragraph_index}. Document has {len(doc.paragraphs)} paragraphs." + para = doc.paragraphs[target_paragraph_index] + found = True + else: + for i, p in enumerate(doc.paragraphs): + # Skip TOC paragraphs + if p.style and p.style.name.lower().startswith("toc"): + continue + if target_text and target_text in p.text: + para = p + found = True + break + if not found or para is None: + return f"Target paragraph not found (by index or text). (TOC paragraphs are skipped in text search)" + # Save anchor index before insertion + if target_paragraph_index is not None: + anchor_index = target_paragraph_index + else: + anchor_index = None + for i, p in enumerate(doc.paragraphs): + if p is para: + anchor_index = i + break + # Determine numbering ID based on bullet_type + num_id = 1 if bullet_type == 'bullet' else 2 + + # Use ListParagraph style for proper list formatting + style_name = None + for candidate in ['List Paragraph', 'ListParagraph', 'Normal']: + try: + _ = doc.styles[candidate] + style_name = candidate + break + except KeyError: + continue + if not style_name: + style_name = None # fallback to default + + new_paras = [] + for item in (list_items or []): + p = doc.add_paragraph(item, style=style_name) + # Add bullet numbering XML - this is the fix! + add_bullet_numbering(p, num_id=num_id, level=0) + new_paras.append(p) + # Move the new paragraphs to the correct position + for p in reversed(new_paras): + if position == 'before': + para._element.addprevious(p._element) + else: + para._element.addnext(p._element) + doc.save(doc_path) + list_type = "bulleted" if bullet_type == 'bullet' else "numbered" + if anchor_index is not None: + return f"{list_type.capitalize()} list with {len(new_paras)} items inserted {position} paragraph (index {anchor_index})." + else: + return f"{list_type.capitalize()} list with {len(new_paras)} items inserted {position} the target paragraph." + except Exception as e: + return f"Failed to insert numbered list: {str(e)}" + + +def is_toc_paragraph(para): + """Devuelve True si el párrafo tiene un estilo de tabla de contenido (TOC).""" + return para.style and para.style.name.upper().startswith("TOC") + + +def is_heading_paragraph(para): + """Devuelve True si el párrafo tiene un estilo de encabezado (Heading 1, Heading 2, etc).""" + return para.style and para.style.name.lower().startswith("heading") + + +# --- Helper: Get style name from a element --- +def get_paragraph_style(el): + from docx.oxml.ns import qn + pPr = el.find(qn('w:pPr')) + if pPr is not None: + pStyle = pPr.find(qn('w:pStyle')) + if pStyle is not None and 'w:val' in pStyle.attrib: + return pStyle.attrib['w:val'] + return None + +# --- Main: Delete everything under a header until next heading/TOC --- +def delete_block_under_header(doc, header_text): + """ + Remove all elements (paragraphs, tables, etc.) after the header (by text) and before the next heading/TOC (by style). + Returns: (header_element, elements_removed) + """ + # Find the header paragraph by text (like delete_paragraph finds by index) + header_para = None + header_idx = None + + for i, para in enumerate(doc.paragraphs): + if para.text.strip().lower() == header_text.strip().lower(): + header_para = para + header_idx = i + break + + if header_para is None: + return None, 0 + + # Find the next heading/TOC paragraph to determine the end of the block + end_idx = None + for i in range(header_idx + 1, len(doc.paragraphs)): + para = doc.paragraphs[i] + if para.style and para.style.name.lower().startswith(('heading', 'título', 'toc')): + end_idx = i + break + + # If no next heading found, delete until end of document + if end_idx is None: + end_idx = len(doc.paragraphs) + + # Remove paragraphs by index (like delete_paragraph does) + removed_count = 0 + for i in range(header_idx + 1, end_idx): + if i < len(doc.paragraphs): # Safety check + para = doc.paragraphs[header_idx + 1] # Always remove the first paragraph after header + p = para._p + p.getparent().remove(p) + removed_count += 1 + + return header_para._p, removed_count + +# --- Usage in replace_paragraph_block_below_header --- +def replace_paragraph_block_below_header( + doc_path: str, + header_text: str, + new_paragraphs: list, + detect_block_end_fn=None, + new_paragraph_style: str = None +) -> str: + """ + Reemplaza todo el contenido debajo de una cabecera (por texto), hasta el siguiente encabezado/TOC (por estilo). + """ + from docx import Document + import os + if not os.path.exists(doc_path): + return f"Document {doc_path} not found." + + doc = Document(doc_path) + + # Find the header paragraph first + header_para = None + header_idx = None + for i, para in enumerate(doc.paragraphs): + para_text = para.text.strip().lower() + is_toc = is_toc_paragraph(para) + if para_text == header_text.strip().lower() and not is_toc: + header_para = para + header_idx = i + break + + if header_para is None: + return f"Header '{header_text}' not found in document." + + # Delete everything under the header using the same document instance + header_el, removed_count = delete_block_under_header(doc, header_text) + + # Now insert new paragraphs after the header (which should still be in the document) + style_to_use = new_paragraph_style or "Normal" + + # Find the header again after deletion (it should still be there) + current_para = header_para + for text in new_paragraphs: + new_para = doc.add_paragraph(text, style=style_to_use) + current_para._element.addnext(new_para._element) + current_para = new_para + + doc.save(doc_path) + return f"Replaced content under '{header_text}' with {len(new_paragraphs)} paragraph(s), style: {style_to_use}, removed {removed_count} elements." + + +def replace_block_between_manual_anchors( + doc_path: str, + start_anchor_text: str, + new_paragraphs: list, + end_anchor_text: str = None, + match_fn=None, + new_paragraph_style: str = None +) -> str: + """ + Replace all content (paragraphs, tables, etc.) between start_anchor_text and end_anchor_text (or next logical header if not provided). + If end_anchor_text is None, deletes until next visually distinct paragraph (bold, all caps, or different font size), or end of document. + Inserts new_paragraphs after the start anchor. + """ + from docx import Document + import os + if not os.path.exists(doc_path): + return f"Document {doc_path} not found." + doc = Document(doc_path) + body = doc.element.body + elements = list(body) + start_idx = None + end_idx = None + # Find start anchor + for i, el in enumerate(elements): + if el.tag == CT_P.tag: + p_text = "".join([node.text or '' for node in el.iter() if node.tag.endswith('}t')]).strip() + if match_fn: + if match_fn(p_text, el): + start_idx = i + break + elif p_text == start_anchor_text.strip(): + start_idx = i + break + if start_idx is None: + return f"Start anchor '{start_anchor_text}' not found." + # Find end anchor + if end_anchor_text: + for i in range(start_idx + 1, len(elements)): + el = elements[i] + if el.tag == CT_P.tag: + p_text = "".join([node.text or '' for node in el.iter() if node.tag.endswith('}t')]).strip() + if match_fn: + if match_fn(p_text, el, is_end=True): + end_idx = i + break + elif p_text == end_anchor_text.strip(): + end_idx = i + break + else: + # Heuristic: next visually distinct paragraph (bold, all caps, or different font size), or end of document + for i in range(start_idx + 1, len(elements)): + el = elements[i] + if el.tag == CT_P.tag: + # Check for bold, all caps, or font size + runs = [node for node in el.iter() if node.tag.endswith('}r')] + for run in runs: + rpr = run.find(qn('w:rPr')) + if rpr is not None: + if rpr.find(qn('w:b')) is not None or rpr.find(qn('w:caps')) is not None or rpr.find(qn('w:sz')) is not None: + end_idx = i + break + if end_idx is not None: + break + # Mark elements for removal + to_remove = [] + for i in range(start_idx + 1, end_idx if end_idx is not None else len(elements)): + to_remove.append(elements[i]) + for el in to_remove: + body.remove(el) + doc.save(doc_path) + # Reload and find start anchor for insertion + doc = Document(doc_path) + paras = doc.paragraphs + anchor_idx = None + for i, para in enumerate(paras): + if para.text.strip() == start_anchor_text.strip(): + anchor_idx = i + break + if anchor_idx is None: + return f"Start anchor '{start_anchor_text}' not found after deletion (unexpected)." + anchor_para = paras[anchor_idx] + style_to_use = new_paragraph_style or "Normal" + for text in new_paragraphs: + new_para = doc.add_paragraph(text, style=style_to_use) + anchor_para._element.addnext(new_para._element) + anchor_para = new_para + doc.save(doc_path) + return f"Replaced content between '{start_anchor_text}' and '{end_anchor_text or 'next logical header'}' with {len(new_paragraphs)} paragraph(s), style: {style_to_use}, removed {len(to_remove)} elements." diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/extended_document_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/extended_document_utils.py new file mode 100644 index 00000000..007d5ce1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/extended_document_utils.py @@ -0,0 +1,165 @@ +""" +Extended document utilities for Word Document Server. +""" +from typing import Dict, List, Any, Tuple +from docx import Document + + +def get_paragraph_text(doc_path: str, paragraph_index: int) -> Dict[str, Any]: + """ + Get text from a specific paragraph in a Word document. + + Args: + doc_path: Path to the Word document + paragraph_index: Index of the paragraph to extract (0-based) + + Returns: + Dictionary with paragraph text and metadata + """ + import os + if not os.path.exists(doc_path): + return {"error": f"Document {doc_path} does not exist"} + + try: + doc = Document(doc_path) + + # Check if paragraph index is valid + if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs): + return {"error": f"Invalid paragraph index: {paragraph_index}. Document has {len(doc.paragraphs)} paragraphs."} + + paragraph = doc.paragraphs[paragraph_index] + + return { + "index": paragraph_index, + "text": paragraph.text, + "style": paragraph.style.name if paragraph.style else "Normal", + "is_heading": paragraph.style.name.startswith("Heading") if paragraph.style else False + } + except Exception as e: + return {"error": f"Failed to get paragraph text: {str(e)}"} + + +def find_text(doc_path: str, text_to_find: str, match_case: bool = True, whole_word: bool = False) -> Dict[str, Any]: + """ + Find all occurrences of specific text in a Word document. + + Args: + doc_path: Path to the Word document + text_to_find: Text to search for + match_case: Whether to perform case-sensitive search + whole_word: Whether to match whole words only + + Returns: + Dictionary with search results + """ + import os + if not os.path.exists(doc_path): + return {"error": f"Document {doc_path} does not exist"} + + if not text_to_find: + return {"error": "Search text cannot be empty"} + + try: + doc = Document(doc_path) + results = { + "query": text_to_find, + "match_case": match_case, + "whole_word": whole_word, + "occurrences": [], + "total_count": 0 + } + + # Search in paragraphs + for i, para in enumerate(doc.paragraphs): + # Prepare text for comparison + para_text = para.text + search_text = text_to_find + + if not match_case: + para_text = para_text.lower() + search_text = search_text.lower() + + # Find all occurrences (simple implementation) + start_pos = 0 + while True: + if whole_word: + # For whole word search, we need to check word boundaries + words = para_text.split() + found = False + for word_idx, word in enumerate(words): + if (word == search_text or + (not match_case and word.lower() == search_text.lower())): + results["occurrences"].append({ + "paragraph_index": i, + "position": word_idx, + "context": para.text[:100] + ("..." if len(para.text) > 100 else "") + }) + results["total_count"] += 1 + found = True + + # Break after checking all words + break + else: + # For substring search + pos = para_text.find(search_text, start_pos) + if pos == -1: + break + + results["occurrences"].append({ + "paragraph_index": i, + "position": pos, + "context": para.text[:100] + ("..." if len(para.text) > 100 else "") + }) + results["total_count"] += 1 + start_pos = pos + len(search_text) + + # Search in tables + for table_idx, table in enumerate(doc.tables): + for row_idx, row in enumerate(table.rows): + for col_idx, cell in enumerate(row.cells): + for para_idx, para in enumerate(cell.paragraphs): + # Prepare text for comparison + para_text = para.text + search_text = text_to_find + + if not match_case: + para_text = para_text.lower() + search_text = search_text.lower() + + # Find all occurrences (simple implementation) + start_pos = 0 + while True: + if whole_word: + # For whole word search, check word boundaries + words = para_text.split() + found = False + for word_idx, word in enumerate(words): + if (word == search_text or + (not match_case and word.lower() == search_text.lower())): + results["occurrences"].append({ + "location": f"Table {table_idx}, Row {row_idx}, Column {col_idx}", + "position": word_idx, + "context": para.text[:100] + ("..." if len(para.text) > 100 else "") + }) + results["total_count"] += 1 + found = True + + # Break after checking all words + break + else: + # For substring search + pos = para_text.find(search_text, start_pos) + if pos == -1: + break + + results["occurrences"].append({ + "location": f"Table {table_idx}, Row {row_idx}, Column {col_idx}", + "position": pos, + "context": para.text[:100] + ("..." if len(para.text) > 100 else "") + }) + results["total_count"] += 1 + start_pos = pos + len(search_text) + + return results + except Exception as e: + return {"error": f"Failed to search for text: {str(e)}"} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/file_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/file_utils.py new file mode 100644 index 00000000..7974707c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/file_utils.py @@ -0,0 +1,85 @@ +""" +File utility functions for Word Document Server. +""" +import os +from typing import Tuple, Optional +import shutil + + +def check_file_writeable(filepath: str) -> Tuple[bool, str]: + """ + Check if a file can be written to. + + Args: + filepath: Path to the file + + Returns: + Tuple of (is_writeable, error_message) + """ + # If file doesn't exist, check if directory is writeable + if not os.path.exists(filepath): + directory = os.path.dirname(filepath) + # If no directory is specified (empty string), use current directory + if directory == '': + directory = '.' + if not os.path.exists(directory): + return False, f"Directory {directory} does not exist" + if not os.access(directory, os.W_OK): + return False, f"Directory {directory} is not writeable" + return True, "" + + # If file exists, check if it's writeable + if not os.access(filepath, os.W_OK): + return False, f"File {filepath} is not writeable (permission denied)" + + # Try to open the file for writing to see if it's locked + try: + with open(filepath, 'a'): + pass + return True, "" + except IOError as e: + return False, f"File {filepath} is not writeable: {str(e)}" + except Exception as e: + return False, f"Unknown error checking file permissions: {str(e)}" + + +def create_document_copy(source_path: str, dest_path: Optional[str] = None) -> Tuple[bool, str, Optional[str]]: + """ + Create a copy of a document. + + Args: + source_path: Path to the source document + dest_path: Optional path for the new document. If not provided, will use source_path + '_copy.docx' + + Returns: + Tuple of (success, message, new_filepath) + """ + if not os.path.exists(source_path): + return False, f"Source document {source_path} does not exist", None + + if not dest_path: + # Generate a new filename if not provided + base, ext = os.path.splitext(source_path) + dest_path = f"{base}_copy{ext}" + + try: + # Simple file copy + shutil.copy2(source_path, dest_path) + return True, f"Document copied to {dest_path}", dest_path + except Exception as e: + return False, f"Failed to copy document: {str(e)}", None + + +def ensure_docx_extension(filename: str) -> str: + """ + Ensure filename has .docx extension. + + Args: + filename: The filename to check + + Returns: + Filename with .docx extension + """ + if not filename.endswith('.docx'): + return filename + '.docx' + return filename diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_mcp_server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_mcp_server.py new file mode 100644 index 00000000..cd92472c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_mcp_server.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +""" +Run script for the Word Document Server. + +This script provides a simple way to start the Word Document Server. +""" + +from word_document_server.main import run_server + +if __name__ == "__main__": + run_server() diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.github/workflows/ci.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.github/workflows/ci.yml new file mode 100644 index 00000000..28be7262 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.github/workflows/ci.yml @@ -0,0 +1,15 @@ +name: CI +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + python-version: ${{ matrix.python-version }} + - run: uv sync + - run: uv run python -c "import server.main; print('OK')" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.gitignore new file mode 100644 index 00000000..c549ca8a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.gitignore @@ -0,0 +1,3 @@ +server/lib/ +server/__pycache__/ +*.dxt \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/LICENSE new file mode 100644 index 00000000..14e9ee94 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Takashi Ishida + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/README.md new file mode 100644 index 00000000..498d3a77 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/README.md @@ -0,0 +1,49 @@ +# arxiv-latex MCP Server +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![GitHub Release](https://img.shields.io/github/v/release/takashiishida/arxiv-latex-mcp)](https://github.com/takashiishida/arxiv-latex-mcp/releases) +[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/takashiishida/arxiv-latex-mcp)](https://archestra.ai/mcp-catalog/takashiishida__arxiv-latex-mcp) + + +An MCP server that enables [Claude Desktop](https://claude.ai/download), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://www.cursor.com/), or other MCP clients to directly access and process arXiv papers by fetching the LaTeX source. It uses [arxiv-to-prompt](https://github.com/takashiishida/arxiv-to-prompt) under the hood to handle downloading and processing the LaTeX. + +Why use the LaTeX source instead of uploading PDFs? Many PDF chat applications often struggle with mathematical content and equation-heavy papers. By utilizing the original LaTeX source code from arXiv papers, the LLM can accurately understand and handle equations and notations. This approach is particularly valuable for fields like computer science, mathematics, and engineering where precise interpretation of mathematical expressions is crucial. + +## Installation + +If you are using Claude Desktop, you can utilize Desktop Extensions by double-clicking on the .dxt file to install. +Download the .dxt file from [here](https://github.com/takashiishida/arxiv-latex-mcp/releases/). +Supported on macOS and Windows (Windows support is experimental). + +Otherwise, you can manually add the following configuration to your config file: +```json +{ + "mcpServers": { + "arxiv-latex-mcp": { + "command": "uv", + "args": [ + "--directory", + "/ABSOLUTE/PATH/TO/arxiv-latex-mcp", + "run", + "server/main.py" + ] + } + } +} +``` + +You may need to replace the `command` field with the full path of `uv`: check this by running `which uv` (MacOS/Linux) or `where uv` (Windows). + +Restart the application after saving the above. + +For Claude Desktop, click on the hammer icon, and you should see the following in the list of "Available MCP tools": +- `get_paper_prompt` — Get the full flattened LaTeX of a paper +- `get_paper_abstract` — Get just the abstract +- `list_paper_sections` — List section headings of a paper +- `get_paper_section` — Get a specific section by path + +## Example +Try asking questions about a paper from arXiv, e.g., "Explain the first theorem in 2202.00395" + +
+ Example of using arXiv LaTeX MCP with Claude Desktop +
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/example.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/example.png new file mode 100644 index 00000000..21d49194 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/example.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/manifest.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/manifest.json new file mode 100644 index 00000000..7857303c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/manifest.json @@ -0,0 +1,37 @@ +{ + "dxt_version": "0.1", + "name": "arxiv-latex-mcp", + "version": "0.2.1", + "description": "MCP server that uses arxiv-to-prompt to fetch and process arXiv LaTeX sources for precise interpretation of mathematical expressions in scientific papers.", + "author": { + "name": "Takashi Ishida", + "url": "https://takashiishida.github.io" + }, + "homepage": "https://github.com/takashiishida/arxiv-latex-mcp", + "documentation": "https://github.com/takashiishida/arxiv-latex-mcp", + "server": { + "type": "python", + "entry_point": "server/main.py", + "mcp_config": { + "command": "python3", + "args": [ + "${__dirname}/server/main.py" + ], + "env": { + "PYTHONPATH": "${__dirname}/server/lib" + } + } + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/takashiishida/arxiv-latex-mcp" + }, + "compatibility": { + "claude_desktop": ">=0.11.4", + "platforms": ["darwin", "win32"], + "runtimes": { + "python": ">=3.10" + } + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/pyproject.toml new file mode 100644 index 00000000..abb1e27e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "arxiv-latex-mcp" +version = "0.2.1" +description = "An MCP server that fetches and processes arXiv papers using LaTeX source for accurate equation handling" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "httpx>=0.28.1", + "mcp[cli]>=1.6.0", + "arxiv-to-prompt>=0.10.0", + "psycopg2-binary>=2.9.11", +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/main.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/main.py new file mode 100644 index 00000000..aafc51b0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/main.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +ArXiv LaTeX MCP Server + +This server provides tools to fetch and process arXiv papers' LaTeX source code +for better mathematical expression interpretation. +""" + +import asyncio +import logging +from typing import Any + +from mcp.server import Server, NotificationOptions +from mcp.server.models import InitializationOptions +import mcp.types as types +from mcp.server.stdio import stdio_server + +from pg_adapter import process_latex_source, list_sections, extract_section + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("arxiv-latex-mcp") + +# Create server instance +server = Server("arxiv-latex-mcp") + +# MCP logging level (can be changed by client via logging/setLevel) +mcp_log_level: types.LoggingLevel = "info" + + +@server.set_logging_level() +async def handle_set_logging_level(level: types.LoggingLevel) -> None: + """Handle logging level changes from the client.""" + global mcp_log_level + mcp_log_level = level + logger.info(f"MCP logging level set to: {level}") + + +async def mcp_log(level: types.LoggingLevel, message: str) -> None: + """Send a log message to the MCP client.""" + MCP_LEVEL_ORDER = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"] + if MCP_LEVEL_ORDER.index(level) >= MCP_LEVEL_ORDER.index(mcp_log_level): + try: + ctx = server.request_context + await ctx.session.send_log_message(level=level, data=message, logger="arxiv-latex-mcp") + except Exception: + pass # Fall back silently if no active session + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + """List available tools.""" + return [ + types.Tool( + name="get_paper_prompt", + description="Get a flattened LaTeX code of a paper from arXiv ID for precise interpretation of mathematical expressions", + inputSchema={ + "type": "object", + "properties": { + "arxiv_id": { + "type": "string", + "description": "The arXiv ID of the paper (e.g., '2403.12345')", + } + }, + "required": ["arxiv_id"], + }, + ), + types.Tool( + name="get_paper_abstract", + description="Get just the abstract of an arXiv paper (faster and cheaper than fetching the full paper)", + inputSchema={ + "type": "object", + "properties": { + "arxiv_id": { + "type": "string", + "description": "The arXiv ID of the paper (e.g., '2403.12345')", + } + }, + "required": ["arxiv_id"], + }, + ), + types.Tool( + name="list_paper_sections", + description="List section headings of an arXiv paper to see its structure", + inputSchema={ + "type": "object", + "properties": { + "arxiv_id": { + "type": "string", + "description": "The arXiv ID of the paper (e.g., '2403.12345')", + } + }, + "required": ["arxiv_id"], + }, + ), + types.Tool( + name="get_paper_section", + description="Get a specific section of an arXiv paper by section path (use list_paper_sections first to find available sections)", + inputSchema={ + "type": "object", + "properties": { + "arxiv_id": { + "type": "string", + "description": "The arXiv ID of the paper (e.g., '2403.12345')", + }, + "section_path": { + "type": "string", + "description": "The section path to extract (e.g., '1', '2.1', 'Introduction'). Use list_paper_sections to find available paths.", + }, + }, + "required": ["arxiv_id", "section_path"], + }, + ), + ] + + +LATEX_RENDER_INSTRUCTIONS = """ + +IMPORTANT INSTRUCTIONS FOR RENDERING: +When discussing this paper, please use dollar sign notation ($...$) for inline equations and double dollar signs ($$...$$) for display equations when providing responses that include LaTeX mathematical expressions. +""" + + +@server.call_tool() +async def handle_call_tool( + name: str, arguments: dict[str, Any] | None +) -> list[types.TextContent]: + """Handle tool calls.""" + if not arguments or "arxiv_id" not in arguments: + raise ValueError("Missing required argument: arxiv_id") + + arxiv_id = arguments["arxiv_id"] + + try: + if name == "get_paper_prompt": + await mcp_log("info", f"Processing arXiv paper: {arxiv_id}") + prompt = process_latex_source(arxiv_id) + result = prompt + LATEX_RENDER_INSTRUCTIONS + await mcp_log("info", f"Successfully processed arXiv paper: {arxiv_id}") + + elif name == "get_paper_abstract": + await mcp_log("info", f"Getting abstract for arXiv paper: {arxiv_id}") + result = process_latex_source(arxiv_id, abstract_only=True) + await mcp_log("info", f"Successfully got abstract for: {arxiv_id}") + + elif name == "list_paper_sections": + await mcp_log("info", f"Listing sections for arXiv paper: {arxiv_id}") + text = process_latex_source(arxiv_id) + sections = list_sections(text) + result = "\n".join(sections) + await mcp_log("info", f"Successfully listed sections for: {arxiv_id}") + + elif name == "get_paper_section": + if "section_path" not in arguments: + raise ValueError("Missing required argument: section_path") + section_path = arguments["section_path"] + await mcp_log("info", f"Getting section '{section_path}' for arXiv paper: {arxiv_id}") + text = process_latex_source(arxiv_id) + result = extract_section(text, section_path) + if result is None: + result = f"Section '{section_path}' not found. Use list_paper_sections to see available sections." + else: + result = result + LATEX_RENDER_INSTRUCTIONS + await mcp_log("info", f"Successfully got section for: {arxiv_id}") + + else: + raise ValueError(f"Unknown tool: {name}") + + return [types.TextContent(type="text", text=result)] + + except Exception as e: + error_msg = f"Error processing arXiv paper {arxiv_id}: {str(e)}" + await mcp_log("error", error_msg) + + return [types.TextContent(type="text", text=error_msg)] + + +async def main(): + """Main entry point for the server.""" + # Run the server using stdio transport + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="arxiv-latex-mcp", + server_version="0.2.1", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/pg_adapter.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/pg_adapter.py new file mode 100644 index 00000000..781e961a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/pg_adapter.py @@ -0,0 +1,130 @@ +"""PostgreSQL adapter replacing arxiv_to_prompt for arxiv-latex-mcp.""" + +import os +import json +import psycopg2 +import psycopg2.extras +from typing import Optional, List + + +def _get_conn(): + return psycopg2.connect( + host=os.environ.get('PG_HOST', 'localhost'), + port=int(os.environ.get('PG_PORT', '5432')), + dbname=os.environ.get('PG_DATABASE', 'toolathlon'), + user=os.environ.get('PG_USER', 'postgres'), + password=os.environ.get('PG_PASSWORD', 'postgres'), + ) + + +def process_latex_source(arxiv_id: str, abstract_only: bool = False) -> str: + """Replace arxiv_to_prompt.process_latex_source with PG lookup.""" + conn = _get_conn() + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute( + "SELECT title, abstract, full_prompt, sections FROM arxiv_latex.papers WHERE id = %s", + (arxiv_id,) + ) + row = cur.fetchone() + + if not row: + raise ValueError(f"Paper {arxiv_id} not found in database") + + if abstract_only: + return row['abstract'] or f"No abstract available for {arxiv_id}" + + return row['full_prompt'] or f"No content available for {arxiv_id}" + finally: + conn.close() + + +def list_sections(text: str) -> List[str]: + """List section headings from the processed text. + + This mimics arxiv_to_prompt.list_sections by parsing section headers + from the full_prompt text. We look for patterns like: + # Section Title + ## Subsection Title + """ + sections = [] + for line in text.split('\n'): + stripped = line.strip() + if stripped.startswith('#'): + # Count heading level + level = 0 + for ch in stripped: + if ch == '#': + level += 1 + else: + break + title = stripped[level:].strip() + if title: + # Generate section path based on numbering + sections.append(title) + return sections + + +def extract_section(text: str, section_path: str) -> Optional[str]: + """Extract a specific section from the processed text. + + Mimics arxiv_to_prompt.extract_section. + """ + lines = text.split('\n') + in_section = False + section_level = 0 + result_lines = [] + + for line in lines: + stripped = line.strip() + if stripped.startswith('#'): + level = 0 + for ch in stripped: + if ch == '#': + level += 1 + else: + break + title = stripped[level:].strip() + + if in_section: + # If we hit a heading at same or higher level, stop + if level <= section_level: + break + result_lines.append(line) + elif title.lower() == section_path.lower() or section_path in title: + in_section = True + section_level = level + result_lines.append(line) + elif in_section: + result_lines.append(line) + + if result_lines: + return '\n'.join(result_lines) + + # Try matching by section number (e.g., "1", "2.1") + # Look for patterns like "1 Introduction", "2.1 Methods" + for i, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith('#'): + level = 0 + for ch in stripped: + if ch == '#': + level += 1 + else: + break + title = stripped[level:].strip() + # Check if section_path matches the number prefix + if title.startswith(section_path + ' ') or title.startswith(section_path + '.'): + in_section = True + section_level = level + result_lines.append(line) + for subsequent_line in lines[i+1:]: + sub_stripped = subsequent_line.strip() + if sub_stripped.startswith('#'): + sub_level = sum(1 for ch in sub_stripped if ch == '#') + if sub_level <= section_level: + break + result_lines.append(subsequent_line) + return '\n'.join(result_lines) if result_lines else None + + return None diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/uv.lock new file mode 100644 index 00000000..d3672599 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/uv.lock @@ -0,0 +1,1042 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "arxiv-latex-mcp" +version = "0.2.1" +source = { virtual = "." } +dependencies = [ + { name = "arxiv-to-prompt" }, + { name = "httpx" }, + { name = "mcp", extra = ["cli"] }, + { name = "psycopg2-binary" }, +] + +[package.metadata] +requires-dist = [ + { name = "arxiv-to-prompt", specifier = ">=0.10.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, +] + +[[package]] +name = "arxiv-to-prompt" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "pyperclip" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/2d/9927ef07075c4d11a130551ebf4d008173966d12705648867742b0767efc/arxiv_to_prompt-0.10.0.tar.gz", hash = "sha256:17a612386dfd8b723784452ff751d7782f5fda9ff79bf0a2faf9eaa6790ae61d", size = 23800, upload-time = "2026-02-15T02:21:51.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/a0/0576a17e1baf903a1998a4f854cc17739db12332d871d123c478d40cc6c9/arxiv_to_prompt-0.10.0-py3-none-any.whl", hash = "sha256:b7cb77901d857edd1c5424c77a0065cfd1bebad9e393f9dc5ec60af17c0101c1", size = 14837, upload-time = "2026-02-15T02:21:49.792Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +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/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" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.24.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +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/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]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" }, + { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" }, + { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" }, + { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[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" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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/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]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/FUNDING.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/FUNDING.yml new file mode 100644 index 00000000..47b92047 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: blazickjp # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/lint.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/lint.yml new file mode 100644 index 00000000..b66e502c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install black + + - name: Check code formatting with Black + run: | + black --check . \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/publish.yml new file mode 100644 index 00000000..1e39e311 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build hatchling + + - name: Build package + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.8.11 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/tests.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/tests.yml new file mode 100644 index 00000000..0ac3ce4b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/tests.yml @@ -0,0 +1,61 @@ +name: Run Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + types: [opened, synchronize, reopened] + +jobs: + test: + strategy: + matrix: + python-version: ["3.11", "3.12"] + os: [ubuntu-latest, windows-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv (Linux/macOS) + if: runner.os != 'Windows' + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install uv (Windows) + if: runner.os == 'Windows' + run: | + # Install uv + iwr -useb https://astral.sh/uv/install.ps1 | iex + # Add uv to PATH + echo "$HOME\.uv\bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv pip install --system pytest pytest-cov pytest-asyncio + uv pip install --system -e ".[test]" + # If you don't have a [test] extra, use: + # uv pip install --system -r requirements-test.txt + # or just: + # uv pip install --system -e . + + # Run tests differently based on platform + - name: Run tests on Linux/macOS + if: runner.os != 'Windows' + run: | + pytest --cov=./ --cov-report=xml -v + + - name: Run tests on Windows + if: runner.os == 'Windows' + run: | + pytest --cov=./ --cov-report=xml -v + + \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.gitignore new file mode 100644 index 00000000..6033070f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.gitignore @@ -0,0 +1,40 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.coverage +*.DS_Store + +# Virtual Environment +venv/ +env/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Logs +*.log + +# Local development settings +.env \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.pre-commit-config.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.pre-commit-config.yaml new file mode 100644 index 00000000..0f7c5c96 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3.11 \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.python-version new file mode 100644 index 00000000..2c073331 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/CLAUDE.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/CLAUDE.md new file mode 100644 index 00000000..561df1a4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Environment Setup +```bash +# Create and activate virtual environment +uv venv +source .venv/bin/activate + +# Install with test dependencies +uv pip install -e ".[test]" +``` + +### Testing +```bash +# Run all tests with coverage +python -m pytest + +# Run specific test file +python -m pytest tests/tools/test_search.py + +# Run tests with verbose output +python -m pytest -v +``` + +### Running the Server +```bash +# Run as module +python -m arxiv_mcp_server + +# Or via entry point +arxiv-mcp-server +``` + +## Architecture Overview + +This is an **MCP (Message Control Protocol) server** that provides AI models access to arXiv research papers. The codebase follows a modular architecture with four main layers: + +### Core Components + +1. **Server Layer** (`server.py`): Main MCP server implementation that handles tool registration and request routing +2. **Tools Layer** (`tools/`): Individual MCP tools for paper operations: + - `search.py`: Advanced arXiv paper search with filtering + - `download.py`: Paper download and storage management + - `list_papers.py`: List locally stored papers + - `read_paper.py`: Read paper content from storage +3. **Resource Management** (`resources/papers.py`): `PaperManager` class handles paper storage, PDF-to-markdown conversion using pymupdf4llm, and local caching +4. **Configuration** (`config.py`): Pydantic-based settings with environment variable support + +### Key Design Patterns + +- **MCP Protocol Compliance**: All tools follow MCP specification with proper type definitions +- **Async-First**: Built on asyncio with aiofiles for non-blocking I/O operations +- **Storage Strategy**: Papers downloaded as PDFs, converted to markdown, stored locally with PDF cleanup +- **Error Handling**: Comprehensive error handling with user-friendly messages throughout tool chain + +### Configuration + +Environment variables (all optional with sensible defaults): +- `ARXIV_STORAGE_PATH`: Paper storage location (default: `~/.arxiv-mcp-server/papers`) +- `ARXIV_MAX_RESULTS`: Search results limit (default: 50) +- `ARXIV_REQUEST_TIMEOUT`: API timeout in seconds (default: 60) + +### Testing Strategy + +Tests use pytest with async support and comprehensive mocking: +- `conftest.py` provides shared fixtures for mock arXiv papers and HTTP responses +- Tests cover both unit-level tool functionality and integration scenarios +- Mock-based approach avoids external API calls during testing \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/Dockerfile new file mode 100644 index 00000000..7929997d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/Dockerfile @@ -0,0 +1,37 @@ +# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile +# Use a Python base image with uv pre-installed +FROM ghcr.io/astral-sh/uv:python3.11-slim AS uv + +# Set the working directory in the container +WORKDIR /app + +# Enable bytecode compilation for better performance +ENV UV_COMPILE_BYTECODE=1 + +# Use copy mode for mounting cache to avoid linking issues +ENV UV_LINK_MODE=copy + +# Install project dependencies using uv +RUN --mount=type=cache,target=/root/.cache/uv --mount=type=bind,source=pyproject.toml,target=pyproject.toml --mount=type=bind,source=uv.lock,target=uv.lock uv sync --frozen --no-install-project --no-dev --no-editable + +# Copy the rest of the application code +ADD . /app + +# Install the application +RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev --no-editable + +# Create a minimal Python environment +FROM python:3.11-slim-bookworm + +# Set the working directory in the container +WORKDIR /app + +# Copy the installed dependencies and the virtual environment +COPY --from=uv /root/.local /root/.local +COPY --from=uv --chown=app:app /app/.venv /app/.venv + +# Set the PATH to include the virtual environment +ENV PATH="/app/.venv/bin:$PATH" + +# Set the default entrypoint +ENTRYPOINT ["python", "-m", "arxiv_mcp_server"] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/README.md new file mode 100644 index 00000000..e12fd22a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/README.md @@ -0,0 +1,195 @@ +[![Twitter Follow](https://img.shields.io/twitter/follow/JoeBlazick?style=social)](https://twitter.com/JoeBlazick) +[![smithery badge](https://smithery.ai/badge/arxiv-mcp-server)](https://smithery.ai/server/arxiv-mcp-server) +[![Python Version](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![Tests](https://github.com/blazickjp/arxiv-mcp-server/actions/workflows/tests.yml/badge.svg)](https://github.com/blazickjp/arxiv-mcp-server/actions/workflows/tests.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![PyPI Downloads](https://img.shields.io/pypi/dm/arxiv-mcp-server.svg)](https://pypi.org/project/arxiv-mcp-server/) +[![PyPI Version](https://img.shields.io/pypi/v/arxiv-mcp-server.svg)](https://pypi.org/project/arxiv-mcp-server/) + +# ArXiv MCP Server + +> 🔍 Enable AI assistants to search and access arXiv papers through a simple MCP interface. + +The ArXiv MCP Server provides a bridge between AI assistants and arXiv's research repository through the Model Context Protocol (MCP). It allows AI models to search for papers and access their content in a programmatic way. + +
+ +🤝 **[Contribute](https://github.com/blazickjp/arxiv-mcp-server/blob/main/CONTRIBUTING.md)** • +📝 **[Report Bug](https://github.com/blazickjp/arxiv-mcp-server/issues)** + +Pulse MCP Badge +
+ +## ✨ Core Features + +- 🔎 **Paper Search**: Query arXiv papers with filters for date ranges and categories +- 📄 **Paper Access**: Download and read paper content +- 📋 **Paper Listing**: View all downloaded papers +- 🗃️ **Local Storage**: Papers are saved locally for faster access +- 📝 **Prompts**: A Set of Research Prompts + +## 🚀 Quick Start + +### Installing via Smithery + +To install ArXiv Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/arxiv-mcp-server): + +```bash +npx -y @smithery/cli install arxiv-mcp-server --client claude +``` + +### Installing Manually +Install using uv: + +```bash +uv tool install arxiv-mcp-server +``` + +For development: + +```bash +# Clone and set up development environment +git clone https://github.com/blazickjp/arxiv-mcp-server.git +cd arxiv-mcp-server + +# Create and activate virtual environment +uv venv +source .venv/bin/activate + +# Install with test dependencies +uv pip install -e ".[test]" +``` + +### 🔌 MCP Integration + +Add this configuration to your MCP client config file: + +```json +{ + "mcpServers": { + "arxiv-mcp-server": { + "command": "uv", + "args": [ + "tool", + "run", + "arxiv-mcp-server", + "--storage-path", "/path/to/paper/storage" + ] + } + } +} +``` + +For Development: + +```json +{ + "mcpServers": { + "arxiv-mcp-server": { + "command": "uv", + "args": [ + "--directory", + "path/to/cloned/arxiv-mcp-server", + "run", + "arxiv-mcp-server", + "--storage-path", "/path/to/paper/storage" + ] + } + } +} +``` + +## 💡 Available Tools + +The server provides four main tools: + +### 1. Paper Search +Search for papers with optional filters: + +```python +result = await call_tool("search_papers", { + "query": "transformer architecture", + "max_results": 10, + "date_from": "2023-01-01", + "categories": ["cs.AI", "cs.LG"] +}) +``` + +### 2. Paper Download +Download a paper by its arXiv ID: + +```python +result = await call_tool("download_paper", { + "paper_id": "2401.12345" +}) +``` + +### 3. List Papers +View all downloaded papers: + +```python +result = await call_tool("list_papers", {}) +``` + +### 4. Read Paper +Access the content of a downloaded paper: + +```python +result = await call_tool("read_paper", { + "paper_id": "2401.12345" +}) +``` + +## 📝 Research Prompts + +The server offers specialized prompts to help analyze academic papers: + +### Paper Analysis Prompt +A comprehensive workflow for analyzing academic papers that only requires a paper ID: + +```python +result = await call_prompt("deep-paper-analysis", { + "paper_id": "2401.12345" +}) +``` + +This prompt includes: +- Detailed instructions for using available tools (list_papers, download_paper, read_paper, search_papers) +- A systematic workflow for paper analysis +- Comprehensive analysis structure covering: + - Executive summary + - Research context + - Methodology analysis + - Results evaluation + - Practical and theoretical implications + - Future research directions + - Broader impacts + +## ⚙️ Configuration + +Configure through environment variables: + +| Variable | Purpose | Default | +|----------|---------|---------| +| `ARXIV_STORAGE_PATH` | Paper storage location | ~/.arxiv-mcp-server/papers | + +## 🧪 Testing + +Run the test suite: + +```bash +python -m pytest +``` + +## 📄 License + +Released under the MIT License. See the LICENSE file for details. + +--- + +
+ +Made with ❤️ by the Pearl Labs Team + +ArXiv Server MCP server +
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/pyproject.toml new file mode 100644 index 00000000..dd4737be --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/pyproject.toml @@ -0,0 +1,77 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "arxiv-mcp-server" +version = "0.3.2" +description = "A flexible arXiv search and analysis service with MCP protocol support" +readme = "README.md" +requires-python = ">=3.11" +license = { text = "MIT" } +authors = [ + { name = "Joseph Blazick", email = "blazickjp@amazon.com" } +] +dependencies = [ + "arxiv>=2.1.0", + "httpx>=0.24.0", + "python-dateutil>=2.8.2", + "pydantic>=2.8.0", + "mcp>=1.2.0", + "pymupdf4llm>=0.0.17", + "aiohttp>=3.9.1", + "python-dotenv>=1.0.0", + "pydantic-settings>=2.1.0", + "aiofiles>=23.2.1", + "uvicorn>=0.30.0", + "sse-starlette>=1.8.2", + "anyio>=4.2.0", + "black>=25.1.0", + "pymupdf-layout>=1.26.6", + "psycopg2-binary>=2.9.11", +] + +[project.optional-dependencies] +test = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.5", + "pytest-cov>=4.1.0", + "pytest-mock>=3.10.0", + "aioresponses>=0.7.6" +] +dev = [ + "black>=23.3.0" +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_fixture_loop_scope = "function" # Added this line +testpaths = ["tests"] +addopts = "-v --cov=arxiv_mcp_server" + +[project.scripts] +arxiv-mcp-server = "arxiv_mcp_server:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/arxiv_mcp_server"] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.black] +line-length = 88 +target-version = ["py311"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/smithery.yaml new file mode 100644 index 00000000..d9164a5c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/smithery.yaml @@ -0,0 +1,17 @@ +# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml + +startCommand: + type: stdio + configSchema: + # JSON Schema defining the configuration options for the MCP. + type: object + required: + - arxivStoragePath + properties: + arxivStoragePath: + type: string + description: The path to store downloaded papers. + commandFunction: + # A function that produces the CLI command to start the MCP on stdio. + |- + (config) => ({ command: 'python', args: ['-m', 'arxiv_mcp_server'], env: { ARXIV_STORAGE_PATH: config.arxivStoragePath } }) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__init__.py new file mode 100644 index 00000000..46fd7d7e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__init__.py @@ -0,0 +1,14 @@ +""" +Arxiv MCP Server initialization +""" + +from . import server +import asyncio + + +def main(): + """Main entry point for the package.""" + asyncio.run(server.main()) + + +__all__ = ["main", "server"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__main__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__main__.py new file mode 100644 index 00000000..2f44a441 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__main__.py @@ -0,0 +1,6 @@ +"""Main entry point for the arxiv-mcp-server package.""" + +from . import main + +if __name__ == "__main__": + main() diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/config.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/config.py new file mode 100644 index 00000000..3ba45b23 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/config.py @@ -0,0 +1,72 @@ +"""Configuration settings for the arXiv MCP server.""" + +import sys +from pydantic_settings import BaseSettings, SettingsConfigDict +from pathlib import Path +import logging + +logger = logging.getLogger(__name__) + + +class Settings(BaseSettings): + """Server configuration settings.""" + + APP_NAME: str = "arxiv-mcp-server" + APP_VERSION: str = "0.3.2" + MAX_RESULTS: int = 50 + BATCH_SIZE: int = 20 + REQUEST_TIMEOUT: int = 60 + HOST: str = "0.0.0.0" + PORT: int = 8000 + model_config = SettingsConfigDict(extra="allow") + + @property + def STORAGE_PATH(self) -> Path: + """Get the resolved storage path and ensure it exists. + + Returns: + Path: The absolute storage path. + """ + path = ( + self._get_storage_path_from_args() + or Path.home() / ".arxiv-mcp-server" / "papers" + ) + path = path.resolve() + path.mkdir(parents=True, exist_ok=True) + return path + + def _get_storage_path_from_args(self) -> Path | None: + """Extract storage path from command line arguments. + + Returns: + Path | None: The storage path if specified in arguments, None otherwise. + """ + args = sys.argv[1:] + + # If not enough arguments + if len(args) < 2: + return None + + # Look for the --storage-path option + try: + storage_path_index = args.index("--storage-path") + except ValueError: + return None + + # Early return if --storage-path is the last argument + if storage_path_index + 1 >= len(args): + return None + + # Try to resolve the path + try: + path = Path(args[storage_path_index + 1]) + return path.resolve() + except (TypeError, ValueError) as e: + # TypeError: If the path argument is not string-like + # ValueError: If the path string is malformed + logger.warning(f"Invalid storage path format: {e}") + except OSError as e: + # OSError: If the path contains invalid characters or is too long + logger.warning(f"Invalid storage path: {e}") + + return None diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/pg_adapter.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/pg_adapter.py new file mode 100644 index 00000000..a8a09f98 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/pg_adapter.py @@ -0,0 +1,296 @@ +"""PostgreSQL adapter replacing arxiv API calls for the arxiv-mcp-server.""" + +import os +import json +import logging +import psycopg2 +import psycopg2.extras +from typing import Dict, Any, List +import mcp.types as types +from .config import Settings +from .tools import search_tool, download_tool, list_tool, read_tool # noqa: F401 + +logger = logging.getLogger("arxiv-mcp-server") +settings = Settings() + + +def _get_conn(): + return psycopg2.connect( + host=os.environ.get('PG_HOST', 'localhost'), + port=int(os.environ.get('PG_PORT', '5432')), + dbname=os.environ.get('PG_DATABASE', 'toolathlon'), + user=os.environ.get('PG_USER', 'postgres'), + password=os.environ.get('PG_PASSWORD', 'postgres'), + ) + + +# ---- search.py replacement ---- + +async def handle_search(arguments: Dict[str, Any]) -> List[types.TextContent]: + """Handle paper search using PostgreSQL instead of arXiv API.""" + try: + max_results = min(int(arguments.get("max_results", 10)), settings.MAX_RESULTS) + base_query = arguments["query"] + date_from = arguments.get("date_from") + date_to = arguments.get("date_to") + categories = arguments.get("categories") + sort_by = arguments.get("sort_by", "relevance") + + conn = _get_conn() + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + conditions = [] + params = [] + + # Full-text search + if base_query.strip(): + conditions.append( + "to_tsvector('english', coalesce(title,'') || ' ' || coalesce(summary,'')) " + "@@ plainto_tsquery('english', %s)" + ) + params.append(base_query) + + # Date filtering + if date_from: + conditions.append("published >= %s::timestamptz") + params.append(date_from) + if date_to: + conditions.append("published <= %s::timestamptz") + params.append(date_to + "T23:59:59Z" if "T" not in date_to else date_to) + + # Category filtering + if categories: + cat_conditions = [] + for cat in categories: + cat_conditions.append("categories @> %s::jsonb") + params.append(json.dumps([cat])) + conditions.append("(" + " OR ".join(cat_conditions) + ")") + + where_clause = " AND ".join(conditions) if conditions else "TRUE" + + # Sort + if sort_by == "date": + order_clause = "published DESC NULLS LAST" + else: + if base_query.strip(): + order_clause = ( + "ts_rank(to_tsvector('english', coalesce(title,'') || ' ' || coalesce(summary,'')), " + "plainto_tsquery('english', %s)) DESC" + ) + params.append(base_query) + else: + order_clause = "published DESC NULLS LAST" + + params.append(max_results) + + sql = f""" + SELECT id, title, authors, summary, categories, primary_category, + published, doi, journal_ref, comment, pdf_url, links + FROM arxiv.papers + WHERE {where_clause} + ORDER BY {order_clause} + LIMIT %s + """ + cur.execute(sql, params) + rows = cur.fetchall() + + # Fallback to ILIKE if FTS returns nothing + if not rows and base_query.strip(): + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(""" + SELECT id, title, authors, summary, categories, primary_category, + published, doi, journal_ref, comment, pdf_url, links + FROM arxiv.papers + WHERE title ILIKE %s OR summary ILIKE %s + LIMIT %s + """, (f'%{base_query}%', f'%{base_query}%', max_results)) + rows = cur.fetchall() + + results = [] + for r in rows: + authors = r['authors'] or [] + if isinstance(authors, list): + authors = [a['name'] if isinstance(a, dict) else str(a) for a in authors] + else: + authors = [str(authors)] + + paper_id = r['id'] + short_id = paper_id.split("v")[0] if "v" in paper_id else paper_id + + results.append({ + "id": short_id, + "title": r['title'], + "authors": authors, + "abstract": r['summary'] or '', + "categories": r['categories'] or [], + "published": r['published'].isoformat() if r['published'] else '', + "url": r.get('pdf_url') or f"http://arxiv.org/pdf/{paper_id}", + "resource_uri": f"arxiv://{short_id}", + }) + + response_data = {"total_results": len(results), "papers": results} + return [types.TextContent(type="text", text=json.dumps(response_data, indent=2))] + + finally: + conn.close() + + except Exception as e: + logger.error(f"PG search error: {e}") + return [types.TextContent(type="text", text=f"Error: {str(e)}")] + + +# ---- download.py replacement ---- + +conversion_statuses: Dict[str, Any] = {} + + +async def handle_download(arguments: Dict[str, Any]) -> List[types.TextContent]: + """Handle paper download using PostgreSQL (mark as downloaded).""" + try: + paper_id = arguments["paper_id"] + check_status = arguments.get("check_status", False) + + conn = _get_conn() + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute("SELECT id, is_downloaded, markdown_content FROM arxiv.papers WHERE id = %s", (paper_id,)) + row = cur.fetchone() + + if not row: + return [types.TextContent(type="text", text=json.dumps({ + "status": "error", + "message": f"Paper {paper_id} not found in database", + }))] + + if check_status: + if row['is_downloaded']: + return [types.TextContent(type="text", text=json.dumps({ + "status": "success", + "message": "Paper is ready", + "resource_uri": f"arxiv://{paper_id}", + }))] + else: + return [types.TextContent(type="text", text=json.dumps({ + "status": "unknown", + "message": "Paper not yet downloaded", + }))] + + if row['is_downloaded']: + return [types.TextContent(type="text", text=json.dumps({ + "status": "success", + "message": "Paper already available", + "resource_uri": f"arxiv://{paper_id}", + }))] + + # Mark as downloaded + with conn.cursor() as cur: + cur.execute( + "UPDATE arxiv.papers SET is_downloaded = TRUE WHERE id = %s", + (paper_id,) + ) + conn.commit() + + return [types.TextContent(type="text", text=json.dumps({ + "status": "success", + "message": "Paper downloaded and converted successfully", + "resource_uri": f"arxiv://{paper_id}", + }))] + + finally: + conn.close() + + except Exception as e: + return [types.TextContent(type="text", text=json.dumps({ + "status": "error", + "message": f"Error: {str(e)}", + }))] + + +# ---- list_papers.py replacement ---- + +async def handle_list_papers(arguments=None) -> List[types.TextContent]: + """List all downloaded papers from PostgreSQL.""" + try: + conn = _get_conn() + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(""" + SELECT id, title, summary, authors, pdf_url, links + FROM arxiv.papers + WHERE is_downloaded = TRUE + """) + rows = cur.fetchall() + + papers = [] + for r in rows: + authors = r['authors'] or [] + if isinstance(authors, list): + authors = [a['name'] if isinstance(a, dict) else str(a) for a in authors] + links_data = r.get('links') or [] + if isinstance(links_data, list): + links = [l['href'] if isinstance(l, dict) else str(l) for l in links_data] + else: + links = [] + + papers.append({ + "title": r['title'], + "summary": r['summary'] or '', + "authors": authors, + "links": links, + "pdf_url": r.get('pdf_url') or f"http://arxiv.org/pdf/{r['id']}", + }) + + response_data = {"total_papers": len(papers), "papers": papers} + return [types.TextContent(type="text", text=json.dumps(response_data, indent=2))] + + finally: + conn.close() + + except Exception as e: + return [types.TextContent(type="text", text=f"Error: {str(e)}")] + + +# ---- read_paper.py replacement ---- + +async def handle_read_paper(arguments: Dict[str, Any]) -> List[types.TextContent]: + """Read paper content from PostgreSQL.""" + try: + paper_id = arguments["paper_id"] + + conn = _get_conn() + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(""" + SELECT id, is_downloaded, markdown_content + FROM arxiv.papers + WHERE id = %s + """, (paper_id,)) + row = cur.fetchone() + + if not row: + return [types.TextContent(type="text", text=json.dumps({ + "status": "error", + "message": f"Paper {paper_id} not found in storage. You may need to download it first using download_paper.", + }))] + + if not row['is_downloaded']: + return [types.TextContent(type="text", text=json.dumps({ + "status": "error", + "message": f"Paper {paper_id} not found in storage. You may need to download it first using download_paper.", + }))] + + content = row['markdown_content'] or '' + return [types.TextContent(type="text", text=json.dumps({ + "status": "success", + "paper_id": paper_id, + "content": content, + }))] + + finally: + conn.close() + + except Exception as e: + return [types.TextContent(type="text", text=json.dumps({ + "status": "error", + "message": f"Error reading paper: {str(e)}", + }))] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/__init__.py new file mode 100644 index 00000000..81ce52cb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/__init__.py @@ -0,0 +1,5 @@ +"""Prompt handling functionality for arXiv MCP server.""" + +from .handlers import list_prompts, get_prompt + +__all__ = ["list_prompts", "get_prompt"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/deep_research_analysis_prompt.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/deep_research_analysis_prompt.py new file mode 100644 index 00000000..5a7e2725 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/deep_research_analysis_prompt.py @@ -0,0 +1,84 @@ +"""Deep research analysis prompt for the arXiv MCP server.""" + +# Consolidated comprehensive paper analysis prompt +PAPER_ANALYSIS_PROMPT = """ +You are an AI research assistant tasked with analyzing academic papers from arXiv. You have access to several tools to help with this analysis: + +AVAILABLE TOOLS: +1. read_paper: Use this tool to retrieve the full content of the paper with the provided arXiv ID +2. download_paper: If the paper is not already available locally, use this tool to download it first +3. search_papers: Find related papers on the same topic to provide context +4. list_papers: Check which papers are already downloaded and available for reading + + + + - First, use the list_papers tool to check if the paper is already downloaded + - If not found, use the download_paper tool to retrieve it + - Then use the read_paper tool with the paper_id to get the full content + - If the paper is not found, use the search_papers tool to find related papers while you wait + - If you find related papers, use the download_paper tool to get the full content of the related papers and read those too + + + - Executive Summary: + * Summarize the paper in 2-3 sentences + * What is the main contribution of the paper? + * What is the main problem that the paper solves? + * What is the main methodology used in the paper? + * What are the main results of the paper? + * What is the main conclusion of the paper? + + + * Research area and specific problem addressed + * Key prior approaches and their limitations + * How this paper aims to advance the field + * How does this paper compare to other papers in the field? + + + * Step-by-step breakdown of the approach + * Key innovations in the methodology + * Theoretical foundations and assumptions + * Technical implementation details + * Algorithmic complexity and performance characteristics + * Anything the reader should know about the methodology if they wanted to replicate the paper + + + * Experimental setup (datasets, benchmarks, metrics) + * Main experimental results and their significance + * Statistical validity and robustness of results + * How results support or challenge the paper's claims + * Comparison to state-of-the-art approaches + + + * How could this be implemented or applied? + * Required resources and potential challenges + * Available code, datasets, or resources + + + * How this work advances fundamental understanding + * New concepts or paradigms introduced + * Challenges to existing theories or assumptions + * Open questions raised + + + * Limitations that future work could address + * Promising follow-up research questions + * Potential for integration with other approaches + * Long-term research agenda this work enables + + + * Societal, ethical, or policy implications + * Environmental or economic considerations + * Potential real-world applications and timeframe + + + + * Use the search_papers tool to find related work or papers building on this work + * Cross-reference findings with other papers you've analyzed + * Use your artifacts to create diagrams, pseudocode, and other visualizations to illustrate key concepts + * Summarize key results in tables for easy reference + + +Structure your analysis with clear headings, maintain technical accuracy while being accessible, and include your critical assessment where appropriate. +Your analysis should be comprehensive but concise. Be sure to critically evaluate the statistical significance and +reproducibility of any reported results. +""" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/handlers.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/handlers.py new file mode 100644 index 00000000..9efea12e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/handlers.py @@ -0,0 +1,104 @@ +"""Handlers for prompt-related requests with paper analysis functionality.""" + +from typing import List, Dict, Optional +from mcp.types import Prompt, PromptMessage, TextContent, GetPromptResult +from .prompts import PROMPTS +from .deep_research_analysis_prompt import PAPER_ANALYSIS_PROMPT + + +# Legacy global research context - used as fallback when no session_id is provided +class ResearchContext: + """Maintains context throughout a research session.""" + + def __init__(self): + self.expertise_level = "intermediate" # default + self.explored_papers = {} # paper_id -> basic metadata + self.paper_analyses = {} # paper_id -> analysis focus and summary + + def update_from_arguments(self, args: Dict[str, str]) -> None: + """Update context based on new arguments.""" + if "expertise_level" in args: + self.expertise_level = args["expertise_level"] + if "paper_id" in args and args["paper_id"] not in self.explored_papers: + self.explored_papers[args["paper_id"]] = {"id": args["paper_id"]} + + +# Global research context for backward compatibility +_research_context = ResearchContext() + +# Output structure for deep paper analysis +OUTPUT_STRUCTURE = """ +Present your analysis with the following structure: +1. Executive Summary: 3-5 sentence overview of key contributions +2. Detailed Analysis: Following the specific focus requested +3. Visual Breakdown: Describe key figures/tables and their significance +4. Related Work Map: Position this paper within the research landscape +5. Implementation Notes: Practical considerations for applying these findings +""" + + +async def list_prompts() -> List[Prompt]: + """Handle prompts/list request.""" + # Filter to only include deep-paper-analysis + return [PROMPTS["deep-paper-analysis"]] if "deep-paper-analysis" in PROMPTS else [] + + +async def get_prompt( + name: str, arguments: Dict[str, str] | None = None, session_id: Optional[str] = None +) -> GetPromptResult: + """Handle prompts/get request for paper analysis. + + Args: + name: The name of the prompt to get + arguments: The arguments to use with the prompt + session_id: Optional user session ID for context persistence + + Returns: + GetPromptResult: The resulting prompt with messages + + Raises: + ValueError: If prompt not found or arguments invalid + """ + if name != "deep-paper-analysis": + raise ValueError(f"Prompt not found: {name}") + + prompt = PROMPTS[name] + if arguments is None: + raise ValueError(f"No arguments provided for prompt: {name}") + + # Validate required arguments + for arg in prompt.arguments: + if arg.required and (arg.name not in arguments or not arguments.get(arg.name)): + raise ValueError(f"Missing required argument: {arg.name}") + + # Use only global research context since research sessions are removed + _research_context.update_from_arguments(arguments) + + # Process deep-paper-analysis prompt + paper_id = arguments.get("paper_id", "") + + # Add context from previous papers if available + previous_papers_context = "" + + # Use global context + if len(_research_context.explored_papers) > 1: + previous_ids = [ + pid for pid in _research_context.explored_papers.keys() if pid != paper_id + ] + if previous_ids: + previous_papers_context = f"\nI've previously analyzed papers: {', '.join(previous_ids)}. If relevant, note connections to these works." + + # Track this analysis in context (for global context only) + _research_context.paper_analyses[paper_id] = {"analysis": "complete"} + + return GetPromptResult( + messages=[ + PromptMessage( + role="user", + content=TextContent( + type="text", + text=f"Analyze paper {paper_id}.{previous_papers_context}\n\n{OUTPUT_STRUCTURE}\n\n{PAPER_ANALYSIS_PROMPT}", + ), + ) + ] + ) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompt_manager.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompt_manager.py new file mode 100644 index 00000000..0e71c2bf --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompt_manager.py @@ -0,0 +1,31 @@ +"""Research journey prompt management for the arXiv MCP server.""" + +from typing import Dict, Optional +from mcp.types import Prompt +from .prompts import PROMPTS + +# Global prompt manager instance +_prompt_manager: Optional[Dict[str, Prompt]] = None + + +def get_prompt_manager() -> Dict[str, Prompt]: + """Get or create the global prompt manager dictionary. + + Returns: + Dict[str, Prompt]: Dictionary of available prompts + """ + global _prompt_manager + if _prompt_manager is None: + _prompt_manager = PROMPTS + + return _prompt_manager + + +def register_prompt(prompt: Prompt) -> None: + """Register a new prompt in the prompt manager. + + Args: + prompt (Prompt): The prompt to register + """ + manager = get_prompt_manager() + manager[prompt.name] = prompt diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompts.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompts.py new file mode 100644 index 00000000..12863f54 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompts.py @@ -0,0 +1,83 @@ +"""Prompt definitions for arXiv MCP server with research journey support.""" + +from mcp.types import ( + Prompt, + PromptArgument, +) + +# Define all prompts +PROMPTS = { + "research-discovery": Prompt( + name="research-discovery", + description="Begin research exploration on a specific topic", + arguments=[ + PromptArgument( + name="topic", description="Research topic or question", required=True + ), + PromptArgument( + name="expertise_level", + description="User's familiarity (beginner/intermediate/expert)", + required=False, + ), + PromptArgument( + name="time_period", + description="Time period for search (e.g., '2023-present')", + required=False, + ), + PromptArgument( + name="domain", + description="Academic domain (e.g., computer_science/physics/biology)", + required=False, + ), + ], + ), + "deep-paper-analysis": Prompt( + name="deep-paper-analysis", + description="Analyze a specific paper in detail", + arguments=[ + PromptArgument( + name="paper_id", description="arXiv paper ID", required=True + ), + ], + ), + "literature-synthesis": Prompt( + name="literature-synthesis", + description="Synthesize findings across multiple papers", + arguments=[ + PromptArgument( + name="paper_ids", + description="Comma-separated list of arXiv paper IDs", + required=True, + ), + PromptArgument( + name="synthesis_type", + description="Synthesis type (themes/methods/timeline/gaps/comprehensive)", + required=False, + ), + PromptArgument( + name="domain", + description="Academic domain (e.g., computer_science/physics/biology)", + required=False, + ), + ], + ), + "research-question": Prompt( + name="research-question", + description="Formulate research questions based on literature", + arguments=[ + PromptArgument( + name="paper_ids", + description="Comma-separated list of arXiv paper IDs", + required=True, + ), + PromptArgument( + name="topic", description="Research topic or question", required=True + ), + PromptArgument( + name="domain", + description="Academic domain (e.g., computer_science/physics/biology)", + required=False, + ), + ], + ), +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/__init__.py new file mode 100644 index 00000000..eab9db79 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/__init__.py @@ -0,0 +1,5 @@ +"""Resource management for the arXiv MCP server.""" + +from .papers import PaperManager + +__all__ = ["PaperManager"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/papers.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/papers.py new file mode 100644 index 00000000..662d13ae --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/papers.py @@ -0,0 +1,101 @@ +"""Resource management and storage for arXiv papers.""" + +from pathlib import Path +from typing import List +import arxiv +import pymupdf4llm +import aiofiles +import logging +from pydantic import AnyUrl +import mcp.types as types +from ..config import Settings + +logger = logging.getLogger("arxiv-mcp-server") + + +class PaperManager: + """Manages the storage, retrieval, and resource handling of arXiv papers.""" + + def __init__(self): + """Initialize the paper management system.""" + settings = Settings() + self.storage_path = Path(settings.STORAGE_PATH) + self.storage_path.mkdir(parents=True, exist_ok=True) + self.client = arxiv.Client() + + def _get_paper_path(self, paper_id: str) -> Path: + """Get the absolute file path for a paper.""" + return self.storage_path / f"{paper_id}.md" + + async def store_paper(self, paper_id: str, pdf_url: str) -> bool: + """Download and store a paper from arXiv.""" + paper_md_path = self._get_paper_path(paper_id) + paper_pdf_path = paper_md_path.with_suffix(".pdf") + + if paper_md_path.exists(): + return True + + try: + paper = next(self.client.results(arxiv.Search(id_list=[paper_id]))) + paper.download_pdf(dirpath=self.storage_path, filename=paper_pdf_path) + markdown = pymupdf4llm.to_markdown(paper_pdf_path, show_progress=False) + + async with aiofiles.open(paper_md_path, "w", encoding="utf-8") as f: + await f.write(markdown) + + return True + + except StopIteration: + raise ValueError(f"Paper with ID {paper_id} not found on arXiv.") + except arxiv.ArxivError as e: + raise ValueError( + f"Error: Failed to download paper {paper_id} from arXiv. Details: {str(e)}" + ) + except Exception as e: + raise ValueError( + f"Error: An unexpected error occurred while storing paper {paper_id}. Details: {str(e)}" + ) + + async def has_paper(self, paper_id: str) -> bool: + """Check if a paper is available in storage.""" + return self._get_paper_path(paper_id).exists() + + async def list_papers(self) -> list[str]: + """List all stored paper IDs.""" + logger.info(f"Listing papers in {self.storage_path}") + paper_ids = [p.stem for p in self.storage_path.glob("*.md")] + logger.info(f"Found {len(paper_ids)} papers") + return paper_ids + + async def list_resources(self) -> List[types.Resource]: + """List all papers as MCP resources with metadata.""" + paper_ids = await self.list_papers() + resources = [] + + for paper_id in paper_ids: + search = arxiv.Search(id_list=[paper_id]) + papers = list(self.client.results(search)) + + if papers: + paper = papers[0] + paper_path = self._get_paper_path(paper_id) + resources.append( + types.Resource( + uri=AnyUrl(f"file://{str(paper_path)}"), + name=paper.title, + description=paper.summary, + mimeType="text/markdown", + ) + ) + + logger.info(f"Found {len(resources)} resources") + return resources + + async def get_paper_content(self, paper_id: str) -> str: + """Get the markdown content of a stored paper.""" + paper_path = self._get_paper_path(paper_id) + if not paper_path.exists(): + raise ValueError(f"Paper {paper_id} not found in storage") + + async with aiofiles.open(paper_path, "r", encoding="utf-8") as f: + return await f.read() diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/server.py new file mode 100644 index 00000000..ea4df470 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/server.py @@ -0,0 +1,81 @@ +""" +Arxiv MCP Server +=============== + +This module implements an MCP server for interacting with arXiv. +""" + +import logging +import mcp.types as types +from typing import Dict, Any, List +from mcp.server import Server +from mcp.server.models import InitializationOptions +from mcp.server import NotificationOptions +from mcp.server.stdio import stdio_server +from .config import Settings +from .pg_adapter import handle_search, handle_download, handle_list_papers, handle_read_paper +from .tools import search_tool, download_tool, list_tool, read_tool +from .prompts.handlers import list_prompts as handler_list_prompts +from .prompts.handlers import get_prompt as handler_get_prompt + +settings = Settings() +logger = logging.getLogger("arxiv-mcp-server") +logger.setLevel(logging.INFO) +server = Server(settings.APP_NAME) + + +@server.list_prompts() +async def list_prompts() -> List[types.Prompt]: + """List available prompts.""" + return await handler_list_prompts() + + +@server.get_prompt() +async def get_prompt( + name: str, arguments: Dict[str, str] | None = None +) -> types.GetPromptResult: + """Get a specific prompt with arguments.""" + return await handler_get_prompt(name, arguments) + + +@server.list_tools() +async def list_tools() -> List[types.Tool]: + """List available arXiv research tools.""" + return [search_tool, download_tool, list_tool, read_tool] + + +@server.call_tool() +async def call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextContent]: + """Handle tool calls for arXiv research functionality.""" + logger.debug(f"Calling tool {name} with arguments {arguments}") + try: + if name == "search_papers": + return await handle_search(arguments) + elif name == "download_paper": + return await handle_download(arguments) + elif name == "list_papers": + return await handle_list_papers(arguments) + elif name == "read_paper": + return await handle_read_paper(arguments) + else: + return [types.TextContent(type="text", text=f"Error: Unknown tool {name}")] + except Exception as e: + logger.error(f"Tool error: {str(e)}") + return [types.TextContent(type="text", text=f"Error: {str(e)}")] + + +async def main(): + """Run the server async context.""" + async with stdio_server() as streams: + await server.run( + streams[0], + streams[1], + InitializationOptions( + server_name=settings.APP_NAME, + server_version=settings.APP_VERSION, + capabilities=server.get_capabilities( + notification_options=NotificationOptions(resources_changed=True), + experimental_capabilities={}, + ), + ), + ) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tool_definitions.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tool_definitions.py new file mode 100644 index 00000000..3cb422b0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tool_definitions.py @@ -0,0 +1,31 @@ +"""Tool definitions for the arXiv MCP server. + +This module contains ONLY tool schema definitions (no external dependencies like arxiv). +Both the original tools and the PG adapter import from here to ensure a single source of truth. +""" + +import mcp.types as types + +search_tool = types.Tool( + name='search_papers', + description='Search for papers on arXiv with advanced filtering and query optimization.\n\nQUERY CONSTRUCTION GUIDELINES:\n- Use QUOTED PHRASES for exact matches: "multi-agent systems", "neural networks", "machine learning"\n- Combine related concepts with OR: "AI agents" OR "software agents" OR "intelligent agents" \n- Use field-specific searches for precision:\n - ti:"exact title phrase" - search in titles only\n - au:"author name" - search by author\n - abs:"keyword" - search in abstracts only\n- Use ANDNOT to exclude unwanted results: "machine learning" ANDNOT "survey"\n- For best results, use 2-4 core concepts rather than long keyword lists\n\nADVANCED SEARCH PATTERNS:\n- Field + phrase: ti:"transformer architecture" for papers with exact title phrase\n- Multiple fields: au:"Smith" AND ti:"quantum" for author Smith\'s quantum papers \n- Exclusions: "deep learning" ANDNOT ("survey" OR "review") to exclude survey papers\n- Broad + narrow: "artificial intelligence" AND (robotics OR "computer vision")\n\nCATEGORY FILTERING (highly recommended for relevance):\n- cs.AI: Artificial Intelligence\n- cs.MA: Multi-Agent Systems \n- cs.LG: Machine Learning\n- cs.CL: Computation and Language (NLP)\n- cs.CV: Computer Vision\n- cs.RO: Robotics\n- cs.HC: Human-Computer Interaction\n- cs.CR: Cryptography and Security\n- cs.DB: Databases\n\nEXAMPLES OF EFFECTIVE QUERIES:\n- ti:"reinforcement learning" with categories: ["cs.LG", "cs.AI"] - for RL papers by title\n- au:"Hinton" AND "deep learning" with categories: ["cs.LG"] - for Hinton\'s deep learning work\n- "multi-agent" ANDNOT "survey" with categories: ["cs.MA"] - exclude survey papers\n- abs:"transformer" AND ti:"attention" with categories: ["cs.CL"] - attention papers with transformer abstracts\n\nDATE FILTERING: Use YYYY-MM-DD format for historical research:\n- date_to: "2015-12-31" - for foundational/classic work (pre-2016)\n- date_from: "2020-01-01" - for recent developments (post-2020)\n- Both together for specific time periods\n\nRESULT QUALITY: Results sorted by RELEVANCE (most relevant papers first), not just newest papers.\nThis ensures you get the most pertinent results regardless of publication date.\n\nTIPS FOR FOUNDATIONAL RESEARCH:\n- Use date_to: "2010-12-31" to find classic papers on BDI, SOAR, ACT-R\n- Combine with field searches: ti:"BDI" AND abs:"belief desire intention" \n- Try author searches: au:"Rao" AND "BDI" for Anand Rao\'s foundational BDI work', + inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'Search query using quoted phrases for exact matches (e.g., \'"machine learning" OR "deep learning"\') or specific technical terms. Avoid overly broad or generic terms.'}, 'max_results': {'type': 'integer', 'description': 'Maximum number of results to return (default: 10, max: 50). Use 15-20 for comprehensive searches.'}, 'date_from': {'type': 'string', 'description': "Start date for papers (YYYY-MM-DD format). Use to find recent work, e.g., '2023-01-01' for last 2 years."}, 'date_to': {'type': 'string', 'description': "End date for papers (YYYY-MM-DD format). Use with date_from to find historical work, e.g., '2020-12-31' for older research."}, 'categories': {'type': 'array', 'items': {'type': 'string'}, 'description': "Strongly recommended: arXiv categories to focus search (e.g., ['cs.AI', 'cs.MA'] for agent research, ['cs.LG'] for ML, ['cs.CL'] for NLP, ['cs.CV'] for vision). Greatly improves relevance."}, 'sort_by': {'type': 'string', 'enum': ['relevance', 'date'], 'description': "Sort results by 'relevance' (most relevant first, default) or 'date' (newest first). Use 'relevance' for focused searches, 'date' for recent developments."}}, 'required': ['query']}, +) + +download_tool = types.Tool( + name='download_paper', + description='Download a paper and create a resource for it', + inputSchema={'type': 'object', 'properties': {'paper_id': {'type': 'string', 'description': 'The arXiv ID of the paper to download'}, 'check_status': {'type': 'boolean', 'description': 'If true, only check conversion status without downloading', 'default': False}}, 'required': ['paper_id']}, +) + +list_tool = types.Tool( + name='list_papers', + description='List all existing papers available as resources', + inputSchema={'type': 'object', 'properties': {}, 'required': []}, +) + +read_tool = types.Tool( + name='read_paper', + description='Read the full content of a stored paper in markdown format', + inputSchema={'type': 'object', 'properties': {'paper_id': {'type': 'string', 'description': 'The arXiv ID of the paper to read'}}, 'required': ['paper_id']}, +) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/__init__.py new file mode 100644 index 00000000..a89cb8c1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/__init__.py @@ -0,0 +1,17 @@ +"""Tool definitions for the arXiv MCP server.""" + +from .search import search_tool, handle_search +from .download import download_tool, handle_download +from .list_papers import list_tool, handle_list_papers +from .read_paper import read_tool, handle_read_paper + +__all__ = [ + "search_tool", + "download_tool", + "read_tool", + "handle_search", + "handle_download", + "handle_read_paper", + "list_tool", + "handle_list_papers", +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/download.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/download.py new file mode 100644 index 00000000..32c4b80c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/download.py @@ -0,0 +1,230 @@ +"""Download functionality for the arXiv MCP server.""" + +import arxiv +import json +import asyncio +from pathlib import Path +from typing import Dict, Any, List, Optional +from dataclasses import dataclass +from datetime import datetime +import mcp.types as types +from ..config import Settings +import pymupdf4llm +import fitz +import logging + +logger = logging.getLogger("arxiv-mcp-server") +settings = Settings() + +# Global dictionary to track conversion status +conversion_statuses: Dict[str, Any] = {} + +fitz.TOOLS.mupdf_display_errors(False) +fitz.TOOLS.mupdf_display_warnings(False) + + +@dataclass +class ConversionStatus: + """Track the status of a PDF to Markdown conversion.""" + + paper_id: str + status: str # 'downloading', 'converting', 'success', 'error' + started_at: datetime + completed_at: Optional[datetime] = None + error: Optional[str] = None + + +download_tool = types.Tool( + name="download_paper", + description="Download a paper and create a resource for it", + inputSchema={ + "type": "object", + "properties": { + "paper_id": { + "type": "string", + "description": "The arXiv ID of the paper to download", + }, + "check_status": { + "type": "boolean", + "description": "If true, only check conversion status without downloading", + "default": False, + }, + }, + "required": ["paper_id"], + }, +) + + +def get_paper_path(paper_id: str, suffix: str = ".md") -> Path: + """Get the absolute file path for a paper with given suffix.""" + storage_path = Path(settings.STORAGE_PATH) + storage_path.mkdir(parents=True, exist_ok=True) + return storage_path / f"{paper_id}{suffix}" + + +def convert_pdf_to_markdown(paper_id: str, pdf_path: Path) -> None: + """Convert PDF to Markdown in a separate thread.""" + try: + logger.info(f"Starting conversion for {paper_id}") + markdown = pymupdf4llm.to_markdown(pdf_path, show_progress=False) + md_path = get_paper_path(paper_id, ".md") + + with open(md_path, "w", encoding="utf-8") as f: + f.write(markdown) + + status = conversion_statuses.get(paper_id) + if status: + status.status = "success" + status.completed_at = datetime.now() + + # Clean up PDF after successful conversion + logger.info(f"Conversion completed for {paper_id}") + + except Exception as e: + logger.error(f"Conversion failed for {paper_id}: {str(e)}") + status = conversion_statuses.get(paper_id) + if status: + status.status = "error" + status.completed_at = datetime.now() + status.error = str(e) + + +async def handle_download(arguments: Dict[str, Any]) -> List[types.TextContent]: + """Handle paper download and conversion requests.""" + try: + paper_id = arguments["paper_id"] + check_status = arguments.get("check_status", False) + + # If only checking status + if check_status: + status = conversion_statuses.get(paper_id) + if not status: + if get_paper_path(paper_id, ".md").exists(): + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "success", + "message": "Paper is ready", + "resource_uri": f"file://{get_paper_path(paper_id, '.md')}", + } + ), + ) + ] + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "unknown", + "message": "No download or conversion in progress", + } + ), + ) + ] + + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": status.status, + "started_at": status.started_at.isoformat(), + "completed_at": ( + status.completed_at.isoformat() + if status.completed_at + else None + ), + "error": status.error, + "message": f"Paper conversion {status.status}", + } + ), + ) + ] + + # Check if paper is already converted + if get_paper_path(paper_id, ".md").exists(): + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "success", + "message": "Paper already available", + "resource_uri": f"file://{get_paper_path(paper_id, '.md')}", + } + ), + ) + ] + + # Check if already in progress + if paper_id in conversion_statuses: + status = conversion_statuses[paper_id] + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": status.status, + "message": f"Paper conversion {status.status}", + "started_at": status.started_at.isoformat(), + } + ), + ) + ] + + # Start new download and conversion + pdf_path = get_paper_path(paper_id, ".pdf") + client = arxiv.Client() + + # Initialize status + conversion_statuses[paper_id] = ConversionStatus( + paper_id=paper_id, status="downloading", started_at=datetime.now() + ) + + # Download PDF + paper = next(client.results(arxiv.Search(id_list=[paper_id]))) + paper.download_pdf(dirpath=pdf_path.parent, filename=pdf_path.name) + + # Update status and start conversion + status = conversion_statuses[paper_id] + status.status = "converting" + + # Start conversion in thread + asyncio.create_task( + asyncio.to_thread(convert_pdf_to_markdown, paper_id, pdf_path) + ) + + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "converting", + "message": "Paper downloaded, conversion started", + "started_at": status.started_at.isoformat(), + } + ), + ) + ] + + except StopIteration: + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "error", + "message": f"Paper {paper_id} not found on arXiv", + } + ), + ) + ] + except Exception as e: + return [ + types.TextContent( + type="text", + text=json.dumps({"status": "error", "message": f"Error: {str(e)}"}), + ) + ] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/list_papers.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/list_papers.py new file mode 100644 index 00000000..f9760711 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/list_papers.py @@ -0,0 +1,58 @@ +"""List functionality for the arXiv MCP server.""" + +import json +from pathlib import Path +import arxiv +from typing import Dict, Any, List, Optional +import mcp.types as types +from ..config import Settings + +settings = Settings() + +list_tool = types.Tool( + name="list_papers", + description="List all existing papers available as resources", + inputSchema={ + "type": "object", + "properties": {}, + "required": [], + }, +) + + +def list_papers() -> list[str]: + """List all stored paper IDs.""" + return [p.stem for p in Path(settings.STORAGE_PATH).glob("*.md")] + + +async def handle_list_papers( + arguments: Optional[Dict[str, Any]] = None, +) -> List[types.TextContent]: + """Handle requests to list all stored papers.""" + try: + papers = list_papers() + + client = arxiv.Client() + + results = client.results(arxiv.Search(id_list=papers)) + + response_data = { + "total_papers": len(papers), + "papers": [ + { + "title": result.title, + "summary": result.summary, + "authors": [author.name for author in result.authors], + "links": [link.href for link in result.links], + "pdf_url": result.pdf_url, + } + for result in results + ], + } + + return [ + types.TextContent(type="text", text=json.dumps(response_data, indent=2)) + ] + + except Exception as e: + return [types.TextContent(type="text", text=f"Error: {str(e)}")] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/read_paper.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/read_paper.py new file mode 100644 index 00000000..cb216d43 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/read_paper.py @@ -0,0 +1,80 @@ +"""Read functionality for the arXiv MCP server.""" + +import json +from pathlib import Path +from typing import Dict, Any, List +import mcp.types as types +from ..config import Settings + +settings = Settings() + +read_tool = types.Tool( + name="read_paper", + description="Read the full content of a stored paper in markdown format", + inputSchema={ + "type": "object", + "properties": { + "paper_id": { + "type": "string", + "description": "The arXiv ID of the paper to read", + } + }, + "required": ["paper_id"], + }, +) + + +def list_papers() -> list[str]: + """List all stored paper IDs.""" + return [p.stem for p in Path(settings.STORAGE_PATH).glob("*.md")] + + +async def handle_read_paper(arguments: Dict[str, Any]) -> List[types.TextContent]: + """Handle requests to read a paper's content.""" + try: + paper_ids = list_papers() + paper_id = arguments["paper_id"] + # Check if paper exists + if paper_id not in paper_ids: + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "error", + "message": f"Paper {paper_id} not found in storage. You may need to download it first using download_paper.", + } + ), + ) + ] + + # Get paper content + content = Path(settings.STORAGE_PATH, f"{paper_id}.md").read_text( + encoding="utf-8" + ) + + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "success", + "paper_id": paper_id, + "content": content, + } + ), + ) + ] + + except Exception as e: + return [ + types.TextContent( + type="text", + text=json.dumps( + { + "status": "error", + "message": f"Error reading paper: {str(e)}", + } + ), + ) + ] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/search.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/search.py new file mode 100644 index 00000000..a3fa7139 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/search.py @@ -0,0 +1,500 @@ +"""Search functionality for the arXiv MCP server.""" + +import arxiv +import json +import logging +import httpx +import xml.etree.ElementTree as ET +from typing import Dict, Any, List, Optional +from datetime import datetime, timezone +from dateutil import parser +import mcp.types as types +from ..config import Settings + +logger = logging.getLogger("arxiv-mcp-server") +settings = Settings() + +# arXiv API endpoint for raw queries (bypasses arxiv package URL encoding issues) +# Use HTTPS to avoid redirect from http -> https +ARXIV_API_URL = "https://export.arxiv.org/api/query" + +# XML namespaces used in arXiv Atom feed +ARXIV_NS = { + "atom": "http://www.w3.org/2005/Atom", + "arxiv": "http://arxiv.org/schemas/atom", +} + +# Valid arXiv category prefixes for validation +VALID_CATEGORIES = { + "cs", + "econ", + "eess", + "math", + "physics", + "q-bio", + "q-fin", + "stat", + "astro-ph", + "cond-mat", + "gr-qc", + "hep-ex", + "hep-lat", + "hep-ph", + "hep-th", + "math-ph", + "nlin", + "nucl-ex", + "nucl-th", + "quant-ph", +} + + +async def _raw_arxiv_search( + query: str, + max_results: int = 10, + sort_by: str = "relevance", + date_from: Optional[str] = None, + date_to: Optional[str] = None, + categories: Optional[List[str]] = None, +) -> List[Dict[str, Any]]: + """ + Perform arXiv search using raw HTTP requests. + + This bypasses the arxiv Python package to avoid URL encoding issues + with date filters. The arxiv package encodes '+' as '%2B' which breaks + the submittedDate:[YYYYMMDD+TO+YYYYMMDD] syntax. + """ + # Build query components + query_parts = [] + + if query.strip(): + query_parts.append(f"({query})") + + # Add category filtering + if categories: + category_filter = " OR ".join(f"cat:{cat}" for cat in categories) + query_parts.append(f"({category_filter})") + + # Add date filtering using arXiv API syntax + if date_from or date_to: + try: + if date_from: + start_date = parser.parse(date_from).strftime("%Y%m%d0000") + else: + start_date = "199107010000" # arXiv started July 1991 + + if date_to: + end_date = parser.parse(date_to).strftime("%Y%m%d2359") + else: + end_date = datetime.now().strftime("%Y%m%d2359") + + # CRITICAL: This must NOT be URL-encoded. The '+' in '+TO+' must remain literal. + date_filter = f"submittedDate:[{start_date}+TO+{end_date}]" + query_parts.append(date_filter) + logger.debug(f"Added date filter: {date_filter}") + except (ValueError, TypeError) as e: + logger.error(f"Error parsing dates: {e}") + raise ValueError(f"Invalid date format. Use YYYY-MM-DD format: {e}") + + if not query_parts: + raise ValueError("No search criteria provided") + + # Combine query parts with AND (space in arXiv = AND) + final_query = " AND ".join(query_parts) + logger.debug(f"Raw API query: {final_query}") + + # Map sort parameter to arXiv API values + sort_map = { + "relevance": "relevance", + "date": "submittedDate", + } + sort_order = "descending" + + # Build the URL manually to avoid encoding the '+' in date ranges + # We encode most parameters but carefully preserve '+TO+' in date filters + base_params = f"max_results={max_results}&sortBy={sort_map.get(sort_by, 'relevance')}&sortOrder={sort_order}" + + # Manually construct search_query parameter + # We need to encode spaces and special chars BUT NOT the '+' in '+TO+' + # Strategy: encode the query parts separately, then join with encoded AND + encoded_query = ( + final_query.replace(" AND ", "+AND+").replace(" OR ", "+OR+").replace(" ", "+") + ) + # But we need to be careful about existing '+TO+' - it should stay as-is + # Since we built the date filter with literal '+TO+', it's already correct + + url = f"{ARXIV_API_URL}?search_query={encoded_query}&{base_params}" + logger.debug(f"Raw API URL: {url}") + + # Make the request + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(url) + response.raise_for_status() + + # Parse the Atom XML response + return _parse_arxiv_atom_response(response.text) + + +def _parse_arxiv_atom_response(xml_text: str) -> List[Dict[str, Any]]: + """Parse arXiv Atom XML response into paper dictionaries.""" + results = [] + + try: + root = ET.fromstring(xml_text) + + for entry in root.findall("atom:entry", ARXIV_NS): + # Extract paper ID from the id URL + id_elem = entry.find("atom:id", ARXIV_NS) + if id_elem is None or id_elem.text is None: + continue + + # ID format: http://arxiv.org/abs/XXXX.XXXXX or http://arxiv.org/abs/category/XXXXXXX + paper_id = id_elem.text.split("/abs/")[-1] + # Remove version suffix for short ID + short_id = paper_id.split("v")[0] if "v" in paper_id else paper_id + + # Title + title_elem = entry.find("atom:title", ARXIV_NS) + title = ( + title_elem.text.strip().replace("\n", " ") + if title_elem is not None and title_elem.text + else "" + ) + + # Authors + authors = [] + for author in entry.findall("atom:author", ARXIV_NS): + name_elem = author.find("atom:name", ARXIV_NS) + if name_elem is not None and name_elem.text: + authors.append(name_elem.text) + + # Abstract/Summary + summary_elem = entry.find("atom:summary", ARXIV_NS) + abstract = ( + summary_elem.text.strip().replace("\n", " ") + if summary_elem is not None and summary_elem.text + else "" + ) + + # Categories + categories = [] + for cat in entry.findall("arxiv:primary_category", ARXIV_NS): + term = cat.get("term") + if term: + categories.append(term) + for cat in entry.findall("atom:category", ARXIV_NS): + term = cat.get("term") + if term and term not in categories: + categories.append(term) + + # Published date + published_elem = entry.find("atom:published", ARXIV_NS) + published = ( + published_elem.text + if published_elem is not None and published_elem.text + else "" + ) + + # PDF URL + pdf_url = None + for link in entry.findall("atom:link", ARXIV_NS): + if link.get("title") == "pdf": + pdf_url = link.get("href") + break + if not pdf_url: + pdf_url = f"http://arxiv.org/pdf/{paper_id}" + + results.append( + { + "id": short_id, + "title": title, + "authors": authors, + "abstract": abstract, + "categories": categories, + "published": published, + "url": pdf_url, + "resource_uri": f"arxiv://{short_id}", + } + ) + + except ET.ParseError as e: + logger.error(f"Failed to parse arXiv XML response: {e}") + raise ValueError(f"Failed to parse arXiv API response: {e}") + + return results + + +search_tool = types.Tool( + name="search_papers", + description="""Search for papers on arXiv with advanced filtering and query optimization. + +QUERY CONSTRUCTION GUIDELINES: +- Use QUOTED PHRASES for exact matches: "multi-agent systems", "neural networks", "machine learning" +- Combine related concepts with OR: "AI agents" OR "software agents" OR "intelligent agents" +- Use field-specific searches for precision: + - ti:"exact title phrase" - search in titles only + - au:"author name" - search by author + - abs:"keyword" - search in abstracts only +- Use ANDNOT to exclude unwanted results: "machine learning" ANDNOT "survey" +- For best results, use 2-4 core concepts rather than long keyword lists + +ADVANCED SEARCH PATTERNS: +- Field + phrase: ti:"transformer architecture" for papers with exact title phrase +- Multiple fields: au:"Smith" AND ti:"quantum" for author Smith's quantum papers +- Exclusions: "deep learning" ANDNOT ("survey" OR "review") to exclude survey papers +- Broad + narrow: "artificial intelligence" AND (robotics OR "computer vision") + +CATEGORY FILTERING (highly recommended for relevance): +- cs.AI: Artificial Intelligence +- cs.MA: Multi-Agent Systems +- cs.LG: Machine Learning +- cs.CL: Computation and Language (NLP) +- cs.CV: Computer Vision +- cs.RO: Robotics +- cs.HC: Human-Computer Interaction +- cs.CR: Cryptography and Security +- cs.DB: Databases + +EXAMPLES OF EFFECTIVE QUERIES: +- ti:"reinforcement learning" with categories: ["cs.LG", "cs.AI"] - for RL papers by title +- au:"Hinton" AND "deep learning" with categories: ["cs.LG"] - for Hinton's deep learning work +- "multi-agent" ANDNOT "survey" with categories: ["cs.MA"] - exclude survey papers +- abs:"transformer" AND ti:"attention" with categories: ["cs.CL"] - attention papers with transformer abstracts + +DATE FILTERING: Use YYYY-MM-DD format for historical research: +- date_to: "2015-12-31" - for foundational/classic work (pre-2016) +- date_from: "2020-01-01" - for recent developments (post-2020) +- Both together for specific time periods + +RESULT QUALITY: Results sorted by RELEVANCE (most relevant papers first), not just newest papers. +This ensures you get the most pertinent results regardless of publication date. + +TIPS FOR FOUNDATIONAL RESEARCH: +- Use date_to: "2010-12-31" to find classic papers on BDI, SOAR, ACT-R +- Combine with field searches: ti:"BDI" AND abs:"belief desire intention" +- Try author searches: au:"Rao" AND "BDI" for Anand Rao's foundational BDI work""", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": 'Search query using quoted phrases for exact matches (e.g., \'"machine learning" OR "deep learning"\') or specific technical terms. Avoid overly broad or generic terms.', + }, + "max_results": { + "type": "integer", + "description": "Maximum number of results to return (default: 10, max: 50). Use 15-20 for comprehensive searches.", + }, + "date_from": { + "type": "string", + "description": "Start date for papers (YYYY-MM-DD format). Use to find recent work, e.g., '2023-01-01' for last 2 years.", + }, + "date_to": { + "type": "string", + "description": "End date for papers (YYYY-MM-DD format). Use with date_from to find historical work, e.g., '2020-12-31' for older research.", + }, + "categories": { + "type": "array", + "items": {"type": "string"}, + "description": "Strongly recommended: arXiv categories to focus search (e.g., ['cs.AI', 'cs.MA'] for agent research, ['cs.LG'] for ML, ['cs.CL'] for NLP, ['cs.CV'] for vision). Greatly improves relevance.", + }, + "sort_by": { + "type": "string", + "enum": ["relevance", "date"], + "description": "Sort results by 'relevance' (most relevant first, default) or 'date' (newest first). Use 'relevance' for focused searches, 'date' for recent developments.", + }, + }, + "required": ["query"], + }, +) + + +def _validate_categories(categories: List[str]) -> bool: + """Validate that all provided categories are valid arXiv categories.""" + for category in categories: + if "." in category: + prefix = category.split(".")[0] + else: + prefix = category + if prefix not in VALID_CATEGORIES: + logger.warning(f"Unknown category prefix: {prefix}") + return False + return True + + +def _optimize_query(query: str) -> str: + """Minimal query optimization - preserve user intent while fixing obvious issues.""" + + # Don't modify queries with existing field specifiers (ti:, au:, abs:, cat:) + if any( + field in query + for field in ["ti:", "au:", "abs:", "cat:", "AND", "OR", "ANDNOT"] + ): + logger.debug("Field-specific or boolean query detected - no optimization") + return query + + # Don't modify queries that are already quoted + if query.startswith('"') and query.endswith('"'): + logger.debug("Pre-quoted query detected - no optimization") + return query + + # For very long queries (>10 terms), suggest user be more specific rather than auto-converting + terms = query.split() + if len(terms) > 10: + logger.warning( + f"Very long query ({len(terms)} terms) - consider using quotes for phrases or field-specific searches" + ) + + # Only optimization: preserve the original query exactly as intended + return query + + +def _process_paper(paper: arxiv.Result) -> Dict[str, Any]: + """Process paper information with resource URI.""" + return { + "id": paper.get_short_id(), + "title": paper.title, + "authors": [author.name for author in paper.authors], + "abstract": paper.summary, + "categories": paper.categories, + "published": paper.published.isoformat(), + "url": paper.pdf_url, + "resource_uri": f"arxiv://{paper.get_short_id()}", + } + + +async def handle_search(arguments: Dict[str, Any]) -> List[types.TextContent]: + """Handle paper search requests with improved arXiv API integration. + + Uses raw HTTP requests when date filtering is requested to avoid URL encoding + issues with the arxiv Python package. Falls back to the arxiv package for + non-date queries for better compatibility. + """ + try: + max_results = min(int(arguments.get("max_results", 10)), settings.MAX_RESULTS) + base_query = arguments["query"] + date_from_arg = arguments.get("date_from") + date_to_arg = arguments.get("date_to") + categories = arguments.get("categories") + sort_by_arg = arguments.get("sort_by", "relevance") + + logger.debug( + f"Starting search with query: '{base_query}', max_results: {max_results}" + ) + + # Validate categories if provided + if categories and not _validate_categories(categories): + return [ + types.TextContent( + type="text", + text="Error: Invalid category provided. Please check arXiv category names.", + ) + ] + + # Use raw HTTP API when date filtering is requested + # This bypasses the arxiv package's URL encoding which breaks date syntax + if date_from_arg or date_to_arg: + logger.debug( + f"Date filtering requested - using raw API: {date_from_arg} to {date_to_arg}" + ) + + try: + optimized_query = ( + _optimize_query(base_query) if base_query.strip() else "" + ) + results = await _raw_arxiv_search( + query=optimized_query, + max_results=max_results, + sort_by=sort_by_arg, + date_from=date_from_arg, + date_to=date_to_arg, + categories=categories, + ) + + logger.info( + f"Raw API search completed: {len(results)} results returned" + ) + response_data = {"total_results": len(results), "papers": results} + + return [ + types.TextContent( + type="text", text=json.dumps(response_data, indent=2) + ) + ] + + except httpx.HTTPStatusError as e: + logger.error(f"arXiv API HTTP error: {e}") + return [ + types.TextContent( + type="text", text=f"Error: arXiv API HTTP error - {str(e)}" + ) + ] + except ValueError as e: + return [types.TextContent(type="text", text=f"Error: {str(e)}")] + + # For non-date queries, use the arxiv package (more robust parsing) + client = arxiv.Client() + + # Build query components + query_parts = [] + + # Add base query with optimization + if base_query.strip(): + optimized_query = _optimize_query(base_query) + query_parts.append(f"({optimized_query})") + if optimized_query != base_query: + logger.debug(f"Optimized query: '{base_query}' -> '{optimized_query}'") + + # Add category filtering + if categories: + category_filter = " OR ".join(f"cat:{cat}" for cat in categories) + query_parts.append(f"({category_filter})") + logger.debug(f"Added category filter: {category_filter}") + + # Combine query parts + if not query_parts: + return [ + types.TextContent( + type="text", text="Error: No search criteria provided" + ) + ] + + # Combine query parts - arXiv uses space for AND by default + final_query = " ".join(query_parts) + logger.debug(f"Final arXiv query: {final_query}") + + # Determine sort method + if sort_by_arg == "date": + sort_criterion = arxiv.SortCriterion.SubmittedDate + logger.debug("Using date sorting (newest first)") + else: + sort_criterion = arxiv.SortCriterion.Relevance + logger.debug("Using relevance sorting (most relevant first)") + + search = arxiv.Search( + query=final_query, + max_results=max_results, + sort_by=sort_criterion, + ) + + # Process results + results = [] + for paper in client.results(search): + if len(results) >= max_results: + break + results.append(_process_paper(paper)) + + logger.info(f"Search completed: {len(results)} results returned") + response_data = {"total_results": len(results), "papers": results} + + return [ + types.TextContent(type="text", text=json.dumps(response_data, indent=2)) + ] + + except arxiv.ArxivError as e: + logger.error(f"ArXiv API error: {e}") + return [ + types.TextContent(type="text", text=f"Error: ArXiv API error - {str(e)}") + ] + except Exception as e: + logger.error(f"Unexpected search error: {e}") + return [types.TextContent(type="text", text=f"Error: {str(e)}")] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/conftest.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/conftest.py new file mode 100644 index 00000000..ea872f2e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/conftest.py @@ -0,0 +1,76 @@ +"""Shared test fixtures for the arXiv MCP server test suite.""" + +import pytest +import tempfile +from datetime import datetime, timezone +from unittest.mock import MagicMock, AsyncMock +import arxiv +from pathlib import Path + + +class MockAuthor: + def __init__(self, name): + self.name = name + + +class MockLink: + def __init__(self, href): + self.href = href + + +@pytest.fixture +def mock_paper(): + """Create a properly structured mock paper with all required attributes.""" + paper = MagicMock(spec=arxiv.Result) + paper.get_short_id.return_value = "2103.12345" + paper.title = "Test Paper" + paper.authors = [MockAuthor("John Doe"), MockAuthor("Jane Smith")] + paper.summary = "Test abstract" + paper.categories = ["cs.AI", "cs.LG"] + paper.published = datetime(2023, 1, 1, tzinfo=timezone.utc) + paper.pdf_url = "https://arxiv.org/pdf/2103.12345" + paper.comment = "Test comment" + paper.journal_ref = "Test Journal 2023" + paper.primary_category = "cs.AI" + paper.links = [MockLink("https://arxiv.org/abs/2103.12345")] + return paper + + +@pytest.fixture +def mock_client(mock_paper): + """Create a mock arxiv client with predefined behavior.""" + client = MagicMock(spec=arxiv.Client) + client.results.return_value = [mock_paper] + return client + + +@pytest.fixture +def temp_storage_path(): + """Create a temporary directory for paper storage during tests.""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +@pytest.fixture +def mock_pdf_content(): + """Create mock PDF content for testing.""" + return b"Mock PDF Content" + + +@pytest.fixture +def mock_http_response(): + """Create a mock HTTP response for testing paper downloads.""" + response = AsyncMock() + response.status = 200 + response.__aenter__.return_value = response + response.read.return_value = b"Mock PDF Content" + return response + + +@pytest.fixture +def mock_http_session(mock_http_response): + """Create a mock HTTP session for testing.""" + session = AsyncMock() + session.get.return_value = mock_http_response + session.__aenter__.return_value = session + return session diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/conftest.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/conftest.py new file mode 100644 index 00000000..1bc07680 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/conftest.py @@ -0,0 +1,62 @@ +"""Test fixtures for prompt tests.""" + +import pytest +from typing import Dict, Any + + +@pytest.fixture +def mock_paper_content() -> str: + """Sample paper content for testing.""" + return """# Test Paper Title + +## Abstract +This is a test paper abstract. + +## Introduction +Test introduction content. + +## Methods +Test methodology section. + +## Results +Test results section. + +## Discussion +Test discussion section. + +## References +1. Test reference +""" + + +@pytest.fixture +def research_discovery_args() -> Dict[str, Any]: + """Sample arguments for research discovery prompt.""" + return { + "topic": "machine learning", + "expertise_level": "intermediate", + "time_period": "2023-present", + } + + +@pytest.fixture +def paper_analysis_args() -> Dict[str, Any]: + """Sample arguments for paper analysis prompt.""" + return {"paper_id": "2401.12345", "focus_area": "methodology"} + + +@pytest.fixture +def literature_synthesis_args() -> Dict[str, Any]: + """Sample arguments for literature synthesis prompt.""" + return {"paper_ids": ["2401.12345", "2401.67890"], "synthesis_type": "themes"} + + +@pytest.fixture(autouse=True) +def clean_paper_manager(): + """Reset the paper manager singleton between tests.""" + # Reset before each test + global paper_manager + paper_manager = None + yield + # Reset after each test + paper_manager = None diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompt_integration.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompt_integration.py new file mode 100644 index 00000000..4760459a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompt_integration.py @@ -0,0 +1,42 @@ +"""Integration tests for prompt functionality.""" + +import pytest +from arxiv_mcp_server.prompts.handlers import list_prompts, get_prompt + + +@pytest.mark.asyncio +async def test_server_list_prompts(): + """Test server list_prompts endpoint.""" + prompts = await list_prompts() + assert len(prompts) == 1 + + # Check that all prompts have required fields + for prompt in prompts: + assert prompt.name + assert prompt.description + assert prompt.arguments is not None + + +@pytest.mark.asyncio +async def test_server_get_analysis_prompt(): + """Test server get_prompt endpoint with analysis prompt.""" + result = await get_prompt("deep-paper-analysis", {"paper_id": "2401.00123"}) + + assert len(result.messages) == 1 + message = result.messages[0] + assert message.role == "user" + assert "2401.00123" in message.content.text + + +@pytest.mark.asyncio +async def test_server_get_prompt_invalid_name(): + """Test server get_prompt endpoint with invalid prompt name.""" + with pytest.raises(ValueError, match="Prompt not found"): + await get_prompt("invalid-prompt", {}) + + +@pytest.mark.asyncio +async def test_server_get_prompt_missing_args(): + """Test server get_prompt endpoint with missing required arguments.""" + with pytest.raises(ValueError, match="Missing required argument"): + await get_prompt("deep-paper-analysis", {}) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompts.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompts.py new file mode 100644 index 00000000..7ccdae2f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompts.py @@ -0,0 +1,53 @@ +"""Unit tests for prompt handlers.""" + +import pytest +from typing import Dict +from arxiv_mcp_server.prompts.handlers import list_prompts, get_prompt +from mcp.types import GetPromptResult, PromptMessage, TextContent + + +@pytest.mark.asyncio +async def test_list_prompts(): + """Test listing available prompts.""" + prompts = await list_prompts() + assert len(prompts) == 1 + + prompt_names = {p.name for p in prompts} + expected_names = {"deep-paper-analysis"} + assert prompt_names == expected_names + + +@pytest.mark.asyncio +async def test_get_paper_analysis_prompt(): + """Test getting paper analysis prompt.""" + result = await get_prompt("deep-paper-analysis", {"paper_id": "2401.00123"}) + + assert isinstance(result, GetPromptResult) + assert len(result.messages) == 1 + message = result.messages[0] + + assert isinstance(message, PromptMessage) + assert message.role == "user" + assert isinstance(message.content, TextContent) + assert "2401.00123" in message.content.text + + +@pytest.mark.asyncio +async def test_get_prompt_with_invalid_name(): + """Test getting prompt with invalid name.""" + with pytest.raises(ValueError, match="Prompt not found"): + await get_prompt("invalid-prompt", {}) + + +@pytest.mark.asyncio +async def test_get_prompt_with_no_arguments(): + """Test getting prompt with no arguments.""" + with pytest.raises(ValueError, match="No arguments provided"): + await get_prompt("deep-paper-analysis", None) + + +@pytest.mark.asyncio +async def test_get_prompt_with_missing_required_argument(): + """Test getting prompt with missing required argument.""" + with pytest.raises(ValueError, match="Missing required argument"): + await get_prompt("deep-paper-analysis", {}) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/test_config.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/test_config.py new file mode 100644 index 00000000..54e862eb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/test_config.py @@ -0,0 +1,131 @@ +"""Tests for the configuration module.""" + +import os +import sys +from pathlib import Path +from arxiv_mcp_server.config import Settings +from unittest.mock import patch + + +@patch.object(Path, "mkdir") +@patch.object(Path, "resolve") +def test_storage_path_default(mock_resolve, mock_mkdir): + """Test that the default storage path is correctly constructed.""" + # Setup the mock to return the path itself when resolved + mock_resolve.side_effect = lambda: Path.home() / ".arxiv-mcp-server" / "papers" + + settings = Settings() + expected_path = Path.home() / ".arxiv-mcp-server" / "papers" + assert settings.STORAGE_PATH == expected_path.resolve() + # Verify mkdir was called with parents=True and exist_ok=True + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + + +@patch.object(Path, "mkdir") +@patch.object(Path, "resolve") +def test_storage_path_from_args(mock_resolve, mock_mkdir): + """Test that the storage path from command line args is correctly parsed.""" + test_path = "/tmp/test_storage" + mock_resolve.side_effect = lambda: Path(test_path) + + with patch.object(sys, "argv", ["program", "--storage-path", test_path]): + settings = Settings() + assert settings.STORAGE_PATH == Path(test_path).resolve() + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + + +@patch.object(Path, "mkdir") +@patch.object(Path, "resolve") +def test_storage_path_platform_compatibility(mock_resolve, mock_mkdir): + """Test that the storage path works correctly on different platforms.""" + # Test with a path format that would be valid on both Windows and Unix + test_paths = [ + # Unix-style path + "/path/to/storage", + # Windows-style path + "C:\\path\\to\\storage", + # Path with spaces + "/path with spaces/to/storage", + # Path with non-ASCII characters + "/path/to/störâgè", + ] + + for test_path in test_paths: + # Reset mocks for each iteration + mock_resolve.reset_mock() + mock_mkdir.reset_mock() + + # Set up the mock to return the path itself + mock_resolve.side_effect = lambda: Path(test_path) + + with patch.object(sys, "argv", ["program", "--storage-path", test_path]): + settings = Settings() + resolved_path = settings.STORAGE_PATH + + # Verify that Path constructor was called with the test path + assert resolved_path == Path(test_path).resolve() + + # Verify that mkdir was called + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + + +def test_storage_path_creates_missing_directory(): + """Test that directories are actually created for the storage path.""" + import tempfile + + # Create a temporary directory for our test + with tempfile.TemporaryDirectory() as tmpdir: + # Create a path that doesn't exist yet + test_path = os.path.join(tmpdir, "deeply", "nested", "directory", "structure") + + # Make sure it doesn't exist yet + assert not os.path.exists(test_path) + + # Patch the arguments to use this path + with patch.object(sys, "argv", ["program", "--storage-path", test_path]): + # Access the STORAGE_PATH property which should create the directories + settings = Settings() + storage_path = settings.STORAGE_PATH + + # Verify the directory was created + assert os.path.exists(test_path) + assert os.path.isdir(test_path) + + # Verify the paths refer to the same location + # Use Path.samefile to handle symlinks (like /var -> /private/var on macOS) + assert Path(storage_path).samefile(test_path) + + +def test_path_normalization_with_windows_paths(): + """Test Windows-specific path handling using string operations only.""" + # Windows-style paths - we'll test the normalization and joining logic + windows_style_paths = [ + # Drive letter with backslashes + "C:\\Users\\username\\Documents\\Papers", + # UNC path (network share) + "\\\\server\\share\\papers", + # Drive letter with forward slashes (also valid on Windows) + "C:/Users/username/Documents/Papers", + # Windows-style path with spaces + "C:\\Program Files\\arXiv\\papers", + # Windows-style path with mixed slashes + "C:\\Users/username\\Documents/Papers", + ] + + # Test that our config works with these path formats + for windows_path in windows_style_paths: + assert Path(windows_path) # This should not raise an error + + # Test path joining logic works correctly + subpath = Path(windows_path) / "subdir" + assert str(subpath).endswith("subdir") + + # The following check is problematic on real Windows systems + # where the path separator may be different + # Check only that the base path is contained in the result (ignoring separator differences) + base_path_norm = windows_path.replace("\\", "/").replace("//", "/") + subpath_norm = str(subpath).replace("\\", "/").replace("//", "/") + assert base_path_norm in subpath_norm + + # Instead of checking exact string equality, verify the Path objects are equivalent + assert subpath == Path(windows_path).joinpath("subdir") diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_download.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_download.py new file mode 100644 index 00000000..234d4133 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_download.py @@ -0,0 +1,81 @@ +"""Tests for paper download functionality.""" + +import pytest +import json +from datetime import datetime +from arxiv_mcp_server.tools.download import ( + handle_download, + get_paper_path, + conversion_statuses, +) + + +@pytest.mark.asyncio +async def test_download_paper_lifecycle(mocker, temp_storage_path): + """Test the complete lifecycle of downloading and converting a paper.""" + paper_id = "2103.12345" + # Mock arxiv client and PDF download + mocker.patch("arxiv.Client.results") + mocker.patch("arxiv.Result.download_pdf") + + # Mock PDF to markdown conversion to happen immediately + async def mock_convert(paper_id, pdf_path): + md_path = get_paper_path(paper_id, ".md") + with open(md_path, "w", encoding="utf-8") as f: + f.write("# Test Paper\nConverted content") + if paper_id in conversion_statuses: + status = conversion_statuses[paper_id] + status.status = "success" + status.completed_at = datetime.now() + pdf_path.unlink() # Cleanup PDF + + mocker.patch("asyncio.to_thread", side_effect=mock_convert) + + # Initial download request + response = await handle_download({"paper_id": paper_id}) + status = json.loads(response[0].text) + assert status["status"] in ["converting", "success"] + + # Check final status + response = await handle_download({"paper_id": paper_id, "check_status": True}) + final_status = json.loads(response[0].text) + assert final_status["status"] in ["success", "converting"] + + # Verify markdown file exists + if final_status["status"] == "success": + assert get_paper_path(paper_id, ".md").exists() + + +@pytest.mark.asyncio +async def test_download_existing_paper(temp_storage_path): + """Test downloading a paper that's already available.""" + paper_id = "2103.12345" + md_path = get_paper_path(paper_id, ".md") + + # Create test markdown file + md_path.parent.mkdir(parents=True, exist_ok=True) + with open(md_path, "w", encoding="utf-8") as f: + f.write("# Existing Paper\nTest content") + + response = await handle_download({"paper_id": paper_id}) + status = json.loads(response[0].text) + assert status["status"] == "success" + + +@pytest.mark.asyncio +async def test_download_nonexistent_paper(mocker): + """Test downloading a paper that doesn't exist.""" + mocker.patch("arxiv.Client.results", side_effect=StopIteration()) + + response = await handle_download({"paper_id": "invalid.12345"}) + status = json.loads(response[0].text) + assert status["status"] == "error" + assert "not found on arXiv" in status["message"] + + +@pytest.mark.asyncio +async def test_check_unknown_status(): + """Test checking status of unknown paper.""" + response = await handle_download({"paper_id": "2103.99999", "check_status": True}) + status = json.loads(response[0].text) + assert status["status"] == "unknown" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_search.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_search.py new file mode 100644 index 00000000..b02907ea --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_search.py @@ -0,0 +1,261 @@ +"""Tests for paper search functionality.""" + +import pytest +import json +from unittest.mock import patch, MagicMock, AsyncMock +from arxiv_mcp_server.tools import handle_search +from arxiv_mcp_server.tools.search import ( + _validate_categories, + _raw_arxiv_search, + _parse_arxiv_atom_response, +) + + +@pytest.mark.asyncio +async def test_basic_search(mock_client): + """Test basic paper search functionality.""" + with patch("arxiv.Client", return_value=mock_client): + result = await handle_search({"query": "test query", "max_results": 1}) + + assert len(result) == 1 + content = json.loads(result[0].text) + assert content["total_results"] == 1 + paper = content["papers"][0] + assert paper["id"] == "2103.12345" + assert paper["title"] == "Test Paper" + assert "resource_uri" in paper + + +@pytest.mark.asyncio +async def test_search_with_categories(mock_client): + """Test paper search with category filtering.""" + with patch("arxiv.Client", return_value=mock_client): + result = await handle_search( + {"query": "test query", "categories": ["cs.AI", "cs.LG"], "max_results": 1} + ) + + content = json.loads(result[0].text) + assert content["papers"][0]["categories"] == ["cs.AI", "cs.LG"] + + +@pytest.mark.asyncio +async def test_search_with_dates(): + """Test paper search with date filtering uses raw API.""" + mock_xml_response = """ + + + http://arxiv.org/abs/2301.00001v1 + Test Paper + Test abstract + 2023-06-15T00:00:00Z + Test Author + + + + """ + + mock_response = MagicMock() + mock_response.text = mock_xml_response + mock_response.raise_for_status = MagicMock() + + with patch("httpx.AsyncClient") as mock_client_class: + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + result = await handle_search( + { + "query": "test query", + "date_from": "2022-01-01", + "date_to": "2024-01-01", + "max_results": 1, + } + ) + + content = json.loads(result[0].text) + assert content["total_results"] == 1 + assert len(content["papers"]) == 1 + + +@pytest.mark.asyncio +async def test_search_with_invalid_dates(): + """Test search with invalid date formats.""" + result = await handle_search( + {"query": "test query", "date_from": "invalid-date", "max_results": 1} + ) + + assert "Error:" in result[0].text + + +def test_validate_categories(): + """Test category validation function.""" + # Valid categories + assert _validate_categories(["cs.AI", "cs.LG"]) + assert _validate_categories(["math.CO", "physics.gen-ph"]) + + # Invalid categories + assert not _validate_categories(["invalid.category"]) + assert not _validate_categories(["cs.AI", "invalid.test"]) + + +def test_parse_arxiv_atom_response(): + """Test parsing of arXiv Atom XML response.""" + sample_xml = """ + + + http://arxiv.org/abs/2301.00001v1 + Test Paper Title + This is a test abstract. + 2023-01-01T00:00:00Z + John Doe + Jane Smith + + + + + + """ + + results = _parse_arxiv_atom_response(sample_xml) + assert len(results) == 1 + paper = results[0] + assert paper["id"] == "2301.00001" + assert paper["title"] == "Test Paper Title" + assert paper["abstract"] == "This is a test abstract." + assert paper["authors"] == ["John Doe", "Jane Smith"] + assert "cs.AI" in paper["categories"] + assert paper["resource_uri"] == "arxiv://2301.00001" + + +@pytest.mark.asyncio +async def test_raw_arxiv_search_builds_correct_url(): + """Test that raw search builds correct URL with date filters.""" + import httpx + + # Mock the httpx client + mock_response = MagicMock() + mock_response.text = """ + + """ + mock_response.raise_for_status = MagicMock() + + with patch("httpx.AsyncClient") as mock_client_class: + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + await _raw_arxiv_search( + query="LLM", + max_results=5, + date_from="2023-01-01", + date_to="2023-12-31", + categories=["cs.AI"], + ) + + # Check that the URL was constructed with unencoded +TO+ + call_args = mock_client.get.call_args + url = call_args[0][0] + assert "+TO+" in url # Critical: must not be encoded as %2B + assert "submittedDate:" in url + assert "20230101" in url + assert "20231231" in url + + +@pytest.mark.asyncio +async def test_search_with_invalid_categories(mock_client): + """Test search with invalid categories.""" + with patch("arxiv.Client", return_value=mock_client): + result = await handle_search( + { + "query": "test query", + "categories": ["invalid.category"], + "max_results": 1, + } + ) + + assert "Error: Invalid category" in result[0].text + + +@pytest.mark.asyncio +async def test_search_empty_query(mock_client): + """Test search with empty query but categories.""" + with patch("arxiv.Client", return_value=mock_client): + result = await handle_search( + {"query": "", "categories": ["cs.AI"], "max_results": 1} + ) + + # Should still work with just categories + content = json.loads(result[0].text) + assert "papers" in content + + +@pytest.mark.asyncio +async def test_search_arxiv_error(mock_client): + """Test handling of arXiv API errors.""" + import arxiv + + # Create proper ArxivError with required parameters + error = arxiv.ArxivError("http://example.com", retry=3, message="API Error") + mock_client.results.side_effect = error + + with patch("arxiv.Client", return_value=mock_client): + result = await handle_search({"query": "test", "max_results": 1}) + + assert "ArXiv API error" in result[0].text + + +@pytest.mark.asyncio +async def test_search_max_results_limiting(mock_client): + """Test that max_results is properly limited.""" + with patch("arxiv.Client", return_value=mock_client): + # Test that very large max_results gets capped + result = await handle_search({"query": "test", "max_results": 1000}) + + # Should not fail and should be limited by settings.MAX_RESULTS + content = json.loads(result[0].text) + assert "papers" in content + + +@pytest.mark.asyncio +async def test_search_sort_by_relevance(mock_client): + """Test search with relevance sorting (default).""" + with patch("arxiv.Client", return_value=mock_client): + result = await handle_search({"query": "test", "sort_by": "relevance"}) + + content = json.loads(result[0].text) + assert "papers" in content + + +@pytest.mark.asyncio +async def test_search_sort_by_date(mock_client): + """Test search with date sorting.""" + with patch("arxiv.Client", return_value=mock_client): + result = await handle_search({"query": "test", "sort_by": "date"}) + + content = json.loads(result[0].text) + assert "papers" in content + + +@pytest.mark.asyncio +async def test_search_no_query_optimization(mock_client): + """Test that queries are not automatically modified.""" + from arxiv_mcp_server.tools.search import _optimize_query + + # Test that complex queries are not mangled + complex_query = "graph neural networks message passing attention mechanism" + optimized = _optimize_query(complex_query) + assert optimized == complex_query + + # Test that field-specific queries are preserved + field_query = 'ti:"graph neural networks"' + optimized = _optimize_query(field_query) + assert optimized == field_query + + # Test that boolean queries are preserved + bool_query = "machine learning AND deep learning" + optimized = _optimize_query(bool_query) + assert optimized == bool_query diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/uv.lock new file mode 100644 index 00000000..d33a1dd4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/uv.lock @@ -0,0 +1,2127 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aioresponses" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/03/532bbc645bdebcf3b6af3b25d46655259d66ce69abba7720b71ebfabbade/aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11", size = 40253, upload-time = "2025-01-19T18:14:03.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b7/584157e43c98aa89810bc2f7099e7e01c728ecf905a66cf705106009228f/aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94", size = 12518, upload-time = "2025-01-19T18:13:59.633Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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/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]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "arxiv" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "feedparser" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/aa/dc1c6c633f63fce090e7c067af8c528a5e61218a61c266ff615d46cbde0a/arxiv-2.4.0.tar.gz", hash = "sha256:cabe5470d031aa3f22d2744a7600391c62c3489653f0c62bec9019e62bb0554b", size = 74546, upload-time = "2026-01-05T02:43:16.823Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/63/9e71153b2d48c98f8079c90d7211bc65515cc1ad18c3328c3c0472e68f44/arxiv-2.4.0-py3-none-any.whl", hash = "sha256:c02ccb09a777aaadd75d3bc1d2627894ef9c987c651d0dacd864b9f69fb0569f", size = 12065, upload-time = "2026-01-05T02:43:12.542Z" }, +] + +[[package]] +name = "arxiv-mcp-server" +version = "0.3.2" +source = { editable = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "anyio" }, + { name = "arxiv" }, + { name = "black" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "psycopg2-binary" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pymupdf-layout" }, + { name = "pymupdf4llm" }, + { name = "python-dateutil" }, + { name = "python-dotenv" }, + { name = "sse-starlette" }, + { name = "uvicorn" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, +] +test = [ + { name = "aioresponses" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = ">=23.2.1" }, + { name = "aiohttp", specifier = ">=3.9.1" }, + { name = "aioresponses", marker = "extra == 'test'", specifier = ">=0.7.6" }, + { name = "anyio", specifier = ">=4.2.0" }, + { name = "arxiv", specifier = ">=2.1.0" }, + { name = "black", specifier = ">=25.1.0" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=23.3.0" }, + { name = "httpx", specifier = ">=0.24.0" }, + { name = "mcp", specifier = ">=1.2.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "pydantic", specifier = ">=2.8.0" }, + { name = "pydantic-settings", specifier = ">=2.1.0" }, + { name = "pymupdf-layout", specifier = ">=1.26.6" }, + { name = "pymupdf4llm", specifier = ">=0.0.17" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.23.5" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" }, + { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.10.0" }, + { name = "python-dateutil", specifier = ">=2.8.2" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "sse-starlette", specifier = ">=1.8.2" }, + { name = "uvicorn", specifier = ">=0.30.0" }, +] +provides-extras = ["test", "dev"] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "black" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +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/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "feedparser" +version = "6.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sgmllib3k" }, +] +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/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]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +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/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]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" }, + { url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" }, + { url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" }, + { url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" }, + { url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/b3240ea84b92a3efb83d49cc16c04a17ade1ab47a6a95c4866d15bf0ac35/onnxruntime-1.24.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a6b4bce87d96f78f0a9bf5cefab3303ae95d558c5bfea53d0bf7f9ea207880a8", size = 17344149, upload-time = "2026-03-05T16:35:13.382Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4a/4b56757e51a56265e8c56764d9c36d7b435045e05e3b8a38bedfc5aedba3/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d48f36c87b25ab3b2b4c88826c96cf1399a5631e3c2c03cc27d6a1e5d6b18eb4", size = 15151571, upload-time = "2026-03-05T16:35:05.679Z" }, + { url = "https://files.pythonhosted.org/packages/cf/14/c6fb84980cec8f682a523fcac7c2bdd6b311e7f342c61ce48d3a9cb87fc6/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e104d33a409bf6e3f30f0e8198ec2aaf8d445b8395490a80f6e6ad56da98e400", size = 17238951, upload-time = "2026-03-05T17:18:12.394Z" }, + { url = "https://files.pythonhosted.org/packages/57/14/447e1400165aca8caf35dabd46540eb943c92f3065927bb4d9bcbc91e221/onnxruntime-1.24.3-cp314-cp314-win_amd64.whl", hash = "sha256:e785d73fbd17421c2513b0bb09eb25d88fa22c8c10c3f5d6060589efa5537c5b", size = 12903820, upload-time = "2026-03-05T17:18:57.123Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/6b2fa5702e4bbba7339ca5787a9d056fc564a16079f8833cc6ba4798da1c/onnxruntime-1.24.3-cp314-cp314-win_arm64.whl", hash = "sha256:951e897a275f897a05ffbcaa615d98777882decaeb80c9216c68cdc62f849f53", size = 12594089, upload-time = "2026-03-05T17:18:47.169Z" }, + { url = "https://files.pythonhosted.org/packages/12/dc/cd06cba3ddad92ceb17b914a8e8d49836c79e38936e26bde6e368b62c1fe/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e70ce578aa214c74c7a7a9226bc8e229814db4a5b2d097333b81279ecde36", size = 15162789, upload-time = "2026-03-05T16:35:08.282Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/413e98ab666c6fb9e8be7d1c6eb3bd403b0bea1b8d42db066dab98c7df07/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02aaf6ddfa784523b6873b4176a79d508e599efe12ab0ea1a3a6e7314408b7aa", size = 17240738, upload-time = "2026-03-05T17:18:15.203Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "7.34.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/00/04a2ab36b70a52d0356852979e08b44edde0435f2115dc66e25f2100f3ab/protobuf-7.34.0.tar.gz", hash = "sha256:3871a3df67c710aaf7bb8d214cc997342e63ceebd940c8c7fc65c9b3d697591a", size = 454726, upload-time = "2026-02-27T00:30:25.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/c4/6322ab5c8f279c4c358bc14eb8aefc0550b97222a39f04eb3c1af7a830fa/protobuf-7.34.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e329966799f2c271d5e05e236459fe1cbfdb8755aaa3b0914fa60947ddea408", size = 429248, upload-time = "2026-02-27T00:30:14.924Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/b029bbbc61e8937545da5b79aa405ab2d9cf307a728f8c9459ad60d7a481/protobuf-7.34.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:9d7a5005fb96f3c1e64f397f91500b0eb371b28da81296ae73a6b08a5b76cdd6", size = 325753, upload-time = "2026-02-27T00:30:17.247Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/09f02671eb75b251c5550a1c48e7b3d4b0623efd7c95a15a50f6f9fc1e2e/protobuf-7.34.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4a72a8ec94e7a9f7ef7fe818ed26d073305f347f8b3b5ba31e22f81fd85fca02", size = 340200, upload-time = "2026-02-27T00:30:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/b5/57/89727baef7578897af5ed166735ceb315819f1c184da8c3441271dbcfde7/protobuf-7.34.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:964cf977e07f479c0697964e83deda72bcbc75c3badab506fb061b352d991b01", size = 324268, upload-time = "2026-02-27T00:30:20.088Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3e/38ff2ddee5cc946f575c9d8cc822e34bde205cf61acf8099ad88ef19d7d2/protobuf-7.34.0-cp310-abi3-win32.whl", hash = "sha256:f791ec509707a1d91bd02e07df157e75e4fb9fbdad12a81b7396201ec244e2e3", size = 426628, upload-time = "2026-02-27T00:30:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/cb/71/7c32eaf34a61a1bae1b62a2ac4ffe09b8d1bb0cf93ad505f42040023db89/protobuf-7.34.0-cp310-abi3-win_amd64.whl", hash = "sha256:9f9079f1dde4e32342ecbd1c118d76367090d4aaa19da78230c38101c5b3dd40", size = 437901, upload-time = "2026-02-27T00:30:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-py3-none-any.whl", hash = "sha256:e3b914dd77fa33fa06ab2baa97937746ab25695f389869afdf03e81f34e45dc7", size = 170716, upload-time = "2026-02-27T00:30:23.994Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymupdf" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/0c/40dda0cc4bd2220a2ef75f8c53dd7d8ed1e29681fcb3df75db6ee9677a7e/pymupdf-1.27.1.tar.gz", hash = "sha256:4afbde0769c336717a149ab0de3330dcb75378f795c1a8c5af55c1a628b17d55", size = 85303479, upload-time = "2026-02-12T08:29:17.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/19/fde6ea4712a904b65e8f41124a0e4233879b87a770fe6a8ce857964de6d5/pymupdf-1.27.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bee9f95512f9556dbf2cacfd1413c61b29a55baa07fa7f8fc83d221d8419888a", size = 23986707, upload-time = "2026-02-11T15:03:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/75/c2/070dff91ad3f1bc16fd6c6ceff23495601fcce4c92d28be534417596418a/pymupdf-1.27.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3de95a0889395b0966fafd11b94980b7543a816e89dd1c218597a08543ac3415", size = 23263493, upload-time = "2026-02-11T15:03:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/937377f4b3e0fbf6273c17436a49f7db17df1a46b1be9e26653b6fafc0e1/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2c9d9353b840040cbc724341f4095fb7e2cc1a12a9147d0ec1a0a79f5d773147", size = 24317651, upload-time = "2026-02-11T22:33:38.967Z" }, + { url = "https://files.pythonhosted.org/packages/72/d5/c701cf2d0cdd6e5d6bca3ca9188d7f5d7ce3ae67dd1368d658cd4bae2707/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:aeaed76e72cbc061149a825ab0811c5f4752970c56591c2938c5042ec06b26e1", size = 24945742, upload-time = "2026-02-11T15:04:06.21Z" }, + { url = "https://files.pythonhosted.org/packages/2b/29/690202b38b93cf77b73a29c25a63a2b6f3fcb36b1f75006e50b8dee7c108/pymupdf-1.27.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4f1837554134fb45d390a44de8844b2ca9b6c901c82ccc90b340e3b7f3b126ca", size = 25167965, upload-time = "2026-02-11T22:36:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/f937e6aa606fd263c3a45d0ff0f0bbdbf3fb779933091fc0f6179513cc93/pymupdf-1.27.1-cp310-abi3-win32.whl", hash = "sha256:fa33b512d82c6c4852edadf57f22d5f27d16243bb33dac0fbe4eb0f281c5b17e", size = 18006253, upload-time = "2026-02-12T13:48:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/3e/99/fe4a7752990bf65277718fffbead4478de9afd1c7288d7a6d643f79a6fa7/pymupdf-1.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:4b6268dff3a9d713034eba5c2ffce0da37c62443578941ac5df433adcde57b2f", size = 19236703, upload-time = "2026-02-11T15:04:19.607Z" }, +] + +[[package]] +name = "pymupdf-layout" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "networkx" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "pymupdf" }, + { name = "pyyaml" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ac/574f537fa0199b31755eb0859d8e6ed8fcc669f7b460eba78611d1d78c8a/pymupdf_layout-1.27.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e60b7238f7652a825a884641fc1e9070e3b385905891ae194dfe91e5932b2342", size = 12323338, upload-time = "2026-02-11T16:23:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0b/b83178bb88cc8933f61dac113c89be9be426fd7c3b9deca3b628e96eb99a/pymupdf_layout-1.27.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:888923391e155cb6a4e0498709c8f4da750858dc273417a7386aeb8b81009b83", size = 12318712, upload-time = "2026-02-11T16:24:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/66/e4/d11ebba2e950606713495620503d0e1b627c879c708fc488f7167cfb35a3/pymupdf_layout-1.27.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:05d88e0060b5875d21ee53c1dc50a2454aa32e59048b971847782318f47dca11", size = 12328732, upload-time = "2026-02-11T22:33:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b6/0278408e2f6db993e3c9d4ee7be89f01b0022233298aa20fe54b95a0169c/pymupdf_layout-1.27.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e9e10d96f06ef5f511523a7caf5b707596a60c5c0a4735f4c5f84728cdb75ffd", size = 12329763, upload-time = "2026-02-11T16:24:09.825Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/f96920afb5a17226e7475f5db9eb7755a019623984caedcb47cb16660bd2/pymupdf_layout-1.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:e456bcdc8760caadcaecb57e96277de11f9cdde785b04bf535448df4fb1e25fc", size = 12332980, upload-time = "2026-02-11T16:24:17.523Z" }, +] + +[[package]] +name = "pymupdf4llm" +version = "0.2.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymupdf" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/25/40ce2f199d97439bff28d9fb3723d68995905b98cba113873d9ff6cfd013/pymupdf4llm-0.2.9.tar.gz", hash = "sha256:6a3e156d1f5724353ae44c18b86b4c5eefcc0d8599eb73a0e92f8137fcbce012", size = 70453, upload-time = "2026-01-10T10:18:00.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/6f/7312cb3d220b252defb1205a5b9d7e074e1c0883d91c461227b2bd3b7fa8/pymupdf4llm-0.2.9-py3-none-any.whl", hash = "sha256:ea9eb3b72765e1970374a37ba7bdc6b1117b0e3ceea5d48b530f49654d8adc94", size = 72286, upload-time = "2026-01-10T10:18:03.805Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/05/3196399a353dd4cd99138a88f662810979ee2f1a1cdb0b417cb2f4507836/pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e", size = 160075, upload-time = "2026-01-19T07:59:00.316Z" }, + { url = "https://files.pythonhosted.org/packages/28/1d/c8fc4ed0a1c4f660391b201cda00b1d5bbcc00e2998e8bcd48b15eefd708/pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165", size = 247318, upload-time = "2026-01-19T07:59:01.636Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/53e55ba01f3e858d229cd84b02481542f42ba59050483a78bf2447ee1af7/pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e", size = 259752, upload-time = "2026-01-19T07:59:04.229Z" }, + { url = "https://files.pythonhosted.org/packages/dc/56/2d930d7f899e3f21868ca6e8ec739ac31e8fc532f66e09cbe45d3df0a84f/pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4", size = 262842, upload-time = "2026-01-19T07:59:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/42/dd/4e7e6920d23deffaf66e6f40d45f7610dcbc132ca5d90ab4faccef22f624/pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b", size = 102620, upload-time = "2026-01-19T07:59:07.839Z" }, + { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" }, + { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" }, + { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" }, + { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" }, + { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/d07e6209f18ef378fc2ae9dee8d1dfe91fd2447c2e2dbfa32867b6dd30cf/pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016", size = 159968, upload-time = "2026-01-19T07:59:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/0a/73/0eb111400abd382a04f253b269819db9fcc748aa40748441cebdcb6d068f/pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b", size = 253373, upload-time = "2026-01-19T07:59:24.381Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8d/9e4e2fdb5bcaba679e54afcc304e9f13f488eb4d626e6b613f9553e03dbd/pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1", size = 267024, upload-time = "2026-01-19T07:59:25.74Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b7/e0a370321af2deb772cff14ff337e1140d1eac2c29a8876bfee995f486f0/pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7", size = 270912, upload-time = "2026-01-19T07:59:27.072Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/4348f916c440d4c3e68b53b4ed0e66b292d119e799fa07afa159566dcc86/pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a", size = 103836, upload-time = "2026-01-19T07:59:28.112Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/a693c0cfa9c783a2a8c4500b7b2a8bab420f8ca4f2d496153226bf1c12e3/pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8", size = 167643, upload-time = "2026-01-19T07:59:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/c0/dd/a64eb1e9f3ec277b69b33ef1b40ffbcc8f0a3bafcde120997efc7bdefebf/pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63", size = 289553, upload-time = "2026-01-19T07:59:30.537Z" }, + { url = "https://files.pythonhosted.org/packages/df/22/06c1079d93dbc3bca5d013e1795f3d8b9ed6c87290acd6913c1c526a6bb2/pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4", size = 302490, upload-time = "2026-01-19T07:59:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/a6f5e43115b4fbf4b93aa87d6c83c79932cdb084f9711daae04549e1e4ad/pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e", size = 305652, upload-time = "2026-01-19T07:59:33.685Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3d/c136e057cb622e36e0c3ff7a8aaa19ff9720050c4078235691da885fe6ee/pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551", size = 115472, upload-time = "2026-01-19T07:59:34.734Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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/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]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "sgmllib3k" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" } + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.github/workflows/python-tests.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.github/workflows/python-tests.yml new file mode 100644 index 00000000..c6b8e46b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.github/workflows/python-tests.yml @@ -0,0 +1,19 @@ +name: Python Tests + +on: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - run: | + python -m pip install --upgrade pip + python -m pip install . + - run: python -m unittest discover -v \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.gitignore new file mode 100644 index 00000000..76118a5c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.gitignore @@ -0,0 +1,13 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# PyCharm +.idea/ \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/CLAUDE.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/CLAUDE.md new file mode 100644 index 00000000..3d3b6da0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a secure Model Context Protocol (MCP) server implementation that provides controlled command-line execution capabilities. The server enables LLM applications to execute CLI commands with comprehensive security features including command whitelisting, path validation, and shell operator protection. + +## Development Commands + +### Testing +```bash +# Run all tests +python -m pytest tests/ + +# Run specific test file +python -m unittest tests.test_cli_mcp_server + +# Run tests with verbose output +python -m unittest -v tests.test_cli_mcp_server +``` + +### Building and Publishing +```bash +# Sync dependencies and update lockfile +uv sync + +# Build package distributions +uv build + +# Publish to PyPI (requires API token) +uv publish --token {YOUR_PYPI_API_TOKEN} +``` + +### Running the Server +```bash +# Run locally for development +uv run cli-mcp-server + +# Run with MCP Inspector for debugging +npx @modelcontextprotocol/inspector uv --directory . run cli-mcp-server +``` + +## Architecture + +### Core Components + +- **`server.py`**: Main MCP server implementation containing: + - `CommandExecutor`: Secure command execution engine with path validation and security controls + - `SecurityConfig`: Configuration dataclass for security policies + - Custom exception classes: `CommandSecurityError`, `CommandExecutionError`, `CommandTimeoutError` + - Two MCP tools: `run_command` and `show_security_rules` + +- **`__init__.py`**: Package entry point that exposes the `main()` function + +### Security Architecture + +The server implements multiple security layers: + +1. **Command Whitelisting**: Only allowed commands can be executed (configurable via `ALLOWED_COMMANDS`) +2. **Flag Validation**: Command flags must be in the allowed list (configurable via `ALLOWED_FLAGS`) +3. **Path Normalization**: All file paths are validated to prevent directory traversal attacks +4. **Shell Operator Control**: Shell operators (&&, ||, |, >, etc.) are disabled by default but can be enabled +5. **Execution Limits**: Configurable timeouts and command length limits +6. **Working Directory Restriction**: Commands execute only within the specified allowed directory + +### Environment Configuration + +Required: +- `ALLOWED_DIR`: Base directory for command execution (must exist) + +Optional (with defaults): +- `ALLOWED_COMMANDS`: Comma-separated commands or "all" (default: "ls,cat,pwd") +- `ALLOWED_FLAGS`: Comma-separated flags or "all" (default: "-l,-a,--help") +- `MAX_COMMAND_LENGTH`: Maximum command string length (default: 1024) +- `COMMAND_TIMEOUT`: Execution timeout in seconds (default: 30) +- `ALLOW_SHELL_OPERATORS`: Enable shell operators like &&, ||, | (default: false) + +**Output Control:** +- `MAX_OUTPUT_LENGTH`: Maximum total output length (default: 10240) +- `MAX_STDOUT_LENGTH`: Maximum stdout length (default: 8192) +- `MAX_STDERR_LENGTH`: Maximum stderr length (default: 2048) +- `OUTPUT_TRUNCATE_MESSAGE`: Message shown when output is truncated (default: "...[output truncated]") + +**Proxy Support:** +- `CLI_PROXY_ENABLED`: Enable proxy support (default: false) +- `CLI_PROXY_URL`: Proxy URL (also checks HTTP_PROXY if not set) + +Example proxy configuration: +```bash +CLI_PROXY_ENABLED=true +CLI_PROXY_URL=http://proxy.company.com:8080 +``` + +## Testing Strategy + +The test suite in `tests/test_cli_mcp_server.py` covers: + +- Basic command execution (pwd, ls) +- Security validation (shell operator blocking) +- Shell operator functionality when enabled +- Path handling and file operations +- Network operations (curl test when available) + +Tests use a temporary directory and reload the server module to test different configurations. + +## Package Management + +- Uses `uv` for dependency management and building +- Python 3.10+ required +- Single dependency: `mcp>=1.10.1` +- Entry point defined in `pyproject.toml` as `cli-mcp-server = "cli_mcp_server:main"` \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/LICENSE new file mode 100644 index 00000000..0d26613a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mladen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/README.md new file mode 100644 index 00000000..e929c66d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/README.md @@ -0,0 +1,318 @@ +# CLI MCP Server + +--- + +A secure Model Context Protocol (MCP) server implementation for executing controlled command-line operations with +comprehensive security features. + +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![Python Version](https://img.shields.io/badge/python-3.10%2B-blue) +![MCP Protocol](https://img.shields.io/badge/MCP-Compatible-green) +[![smithery badge](https://smithery.ai/badge/cli-mcp-server)](https://smithery.ai/protocol/cli-mcp-server) +[![Python Tests](https://github.com/MladenSU/cli-mcp-server/actions/workflows/python-tests.yml/badge.svg)](https://github.com/MladenSU/cli-mcp-server/actions/workflows/python-tests.yml) + + + +--- + +# Table of Contents + +1. [Overview](#overview) +2. [Features](#features) +3. [Configuration](#configuration) +4. [Available Tools](#available-tools) + - [run_command](#run_command) + - [show_security_rules](#show_security_rules) +5. [Usage with Claude Desktop](#usage-with-claude-desktop) + - [Development/Unpublished Servers Configuration](#developmentunpublished-servers-configuration) + - [Published Servers Configuration](#published-servers-configuration) +6. [Security Features](#security-features) +7. [Error Handling](#error-handling) +8. [Development](#development) + - [Prerequisites](#prerequisites) + - [Building and Publishing](#building-and-publishing) + - [Debugging](#debugging) +9. [License](#license) + +--- + +## Overview + +This MCP server enables secure command-line execution with robust security measures including command whitelisting, path +validation, and execution controls. Perfect for providing controlled CLI access to LLM applications while maintaining security. + +## Features + +- 🔒 Secure command execution with strict validation +- ⚙️ Configurable command and flag whitelisting with 'all' option +- 🛡️ Path traversal prevention and validation +- 🚫 Shell operator injection protection +- ⏱️ Execution timeouts and length limits +- 📝 Detailed error reporting +- 🔄 Async operation support +- 🎯 Working directory restriction and validation +- 🌐 HTTP/HTTPS proxy support for network commands +- 📏 Configurable output length limits with truncation +- 🔧 Robust configuration with error handling and fallbacks + +## Configuration + +Configure the server using environment variables: + +### Basic Security Configuration + +| Variable | Description | Default | +|---------------------|------------------------------------------------------|-------------------| +| `ALLOWED_DIR` | Base directory for command execution (Required) | None (Required) | +| `ALLOWED_COMMANDS` | Comma-separated list of allowed commands or 'all' | `ls,cat,pwd` | +| `ALLOWED_FLAGS` | Comma-separated list of allowed flags or 'all' | `-l,-a,--help` | +| `MAX_COMMAND_LENGTH`| Maximum command string length | `1024` | +| `COMMAND_TIMEOUT` | Command execution timeout (seconds) | `30` | +| `ALLOW_SHELL_OPERATORS` | Allow shell operators (&&, \|\|, \|, >, etc.) | `false` | + +### Output Control Configuration + +| Variable | Description | Default | +|-------------------------|---------------------------------------------------|------------------------| +| `MAX_OUTPUT_LENGTH` | Maximum total output length (characters) | `10240` | +| `MAX_STDOUT_LENGTH` | Maximum stdout length (characters) | `8192` | +| `MAX_STDERR_LENGTH` | Maximum stderr length (characters) | `2048` | +| `OUTPUT_TRUNCATE_MESSAGE` | Message shown when output is truncated | `...[output truncated]` | + +### Proxy Configuration + +| Variable | Description | Default | +|---------------------|------------------------------------------------------|-------------------| +| `CLI_PROXY_ENABLED` | Enable proxy support for HTTP/HTTPS requests | `false` | +| `CLI_PROXY_URL` | Proxy URL (also checks HTTP_PROXY if not set) | None | + +**Proxy Usage Examples:** +```bash +# Enable proxy with custom URL +CLI_PROXY_ENABLED=true +CLI_PROXY_URL=http://proxy.company.com:8080 + +# Or use standard HTTP_PROXY environment variable +CLI_PROXY_ENABLED=true +HTTP_PROXY=http://proxy.company.com:8080 +``` + +**Output Control Examples:** +```bash +# Limit stdout to 4KB and stderr to 1KB +MAX_STDOUT_LENGTH=4096 +MAX_STDERR_LENGTH=1024 +OUTPUT_TRUNCATE_MESSAGE="... [Output truncated due to length limit]" + +# Disable output limiting (set to very large values) +MAX_STDOUT_LENGTH=1000000 +MAX_STDERR_LENGTH=1000000 +``` + +Note: Setting `ALLOWED_COMMANDS` or `ALLOWED_FLAGS` to 'all' will allow any command or flag respectively. + +## Common Use Cases + +### Corporate Environment with Proxy +```bash +# Enable proxy for all HTTP/HTTPS commands like curl, wget +CLI_PROXY_ENABLED=true +CLI_PROXY_URL=http://proxy.corporate.com:8080 +ALLOWED_COMMANDS=ls,cat,pwd,curl,wget +ALLOWED_FLAGS=all +``` + +### Limited Output for Performance +```bash +# Prevent memory issues with large command outputs +MAX_STDOUT_LENGTH=4096 +MAX_STDERR_LENGTH=1024 +OUTPUT_TRUNCATE_MESSAGE="... [Output limited for performance]" +``` + +### Development Environment +```bash +# Allow most commands but limit output size +ALLOWED_COMMANDS=all +ALLOWED_FLAGS=all +ALLOW_SHELL_OPERATORS=true +MAX_STDOUT_LENGTH=16384 +CLI_PROXY_ENABLED=true +``` + +## Installation + +To install CLI MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/cli-mcp-server): + +```bash +npx @smithery/cli install cli-mcp-server --client claude +``` + +## Available Tools + +### run_command + +Executes whitelisted CLI commands within allowed directories. + +**Input Schema:** +```json +{ + "command": { + "type": "string", + "description": "Single command to execute (e.g., 'ls -l' or 'cat file.txt')" + } +} +``` + +**Security Notes:** +- Shell operators (&&, |, >, >>) are not supported by default, but can be enabled with `ALLOW_SHELL_OPERATORS=true` +- Commands must be whitelisted unless ALLOWED_COMMANDS='all' +- Flags must be whitelisted unless ALLOWED_FLAGS='all' +- All paths are validated to be within ALLOWED_DIR + +### show_security_rules + +Displays current security configuration and restrictions, including: +- Working directory +- Allowed commands +- Allowed flags +- Security limits (max command length and timeout) +- Output length limits +- Proxy configuration status + +## Usage with Claude Desktop + +Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`: + +> Development/Unpublished Servers Configuration + +```json +{ + "mcpServers": { + "cli-mcp-server": { + "command": "uv", + "args": [ + "--directory", + "/cli-mcp-server", + "run", + "cli-mcp-server" + ], + "env": { + "ALLOWED_DIR": "", + "ALLOWED_COMMANDS": "ls,cat,pwd,echo,curl", + "ALLOWED_FLAGS": "-l,-a,--help,--version,-s,-G", + "MAX_COMMAND_LENGTH": "1024", + "COMMAND_TIMEOUT": "30", + "ALLOW_SHELL_OPERATORS": "false", + "MAX_STDOUT_LENGTH": "8192", + "MAX_STDERR_LENGTH": "2048", + "CLI_PROXY_ENABLED": "false", + "CLI_PROXY_URL": "http://proxy.company.com:8080", + "OUTPUT_TRUNCATE_MESSAGE": "...[output truncated]" + } + } + } +} +``` + +> Published Servers Configuration + +```json +{ + "mcpServers": { + "cli-mcp-server": { + "command": "uvx", + "args": [ + "cli-mcp-server" + ], + "env": { + "ALLOWED_DIR": "", + "ALLOWED_COMMANDS": "ls,cat,pwd,echo,curl", + "ALLOWED_FLAGS": "-l,-a,--help,--version,-s,-G", + "MAX_COMMAND_LENGTH": "1024", + "COMMAND_TIMEOUT": "30", + "ALLOW_SHELL_OPERATORS": "false", + "MAX_STDOUT_LENGTH": "8192", + "MAX_STDERR_LENGTH": "2048", + "CLI_PROXY_ENABLED": "false" + } + } + } +} +``` +> In case it's not working or showing in the UI, clear your cache via `uv clean`. + +## Security Features + +- ✅ Command whitelist enforcement with 'all' option +- ✅ Flag validation with 'all' option +- ✅ Path traversal prevention and normalization +- ✅ Shell operator blocking (with opt-in support via `ALLOW_SHELL_OPERATORS=true`) +- ✅ Command length limits +- ✅ Execution timeouts +- ✅ Working directory restrictions +- ✅ Symlink resolution and validation +- ✅ Output length limiting with configurable truncation +- ✅ Robust environment variable validation with fallbacks +- ✅ Proxy support with secure environment variable handling + +## Error Handling + +The server provides detailed error messages for: + +- Security violations (CommandSecurityError) +- Command timeouts (CommandTimeoutError) +- Invalid command formats +- Path security violations +- Execution failures (CommandExecutionError) +- General command errors (CommandError) + +## Development + +### Prerequisites + +- Python 3.10+ +- MCP protocol library + +### Building and Publishing + +To prepare the package for distribution: + +1. Sync dependencies and update lockfile: + ```bash + uv sync + ``` + +2. Build package distributions: + ```bash + uv build + ``` + + > This will create source and wheel distributions in the `dist/` directory. + +3. Publish to PyPI: + ```bash + uv publish --token {{YOUR_PYPI_API_TOKEN}} + ``` + +### Debugging + +Since MCP servers run over stdio, debugging can be challenging. For the best debugging +experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). + +You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with +this command: + +```bash +npx @modelcontextprotocol/inspector uv --directory {{your source code local directory}}/cli-mcp-server run cli-mcp-server +``` + +Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +For more information or support, please open an issue on the project repository. \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/glama.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/glama.json new file mode 100644 index 00000000..61649d65 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/glama.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://glama.ai/mcp/schemas/server.json", + "maintainers": [ + "MladenSU" + ] +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/pyproject.toml new file mode 100644 index 00000000..8c297021 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "cli-mcp-server" +version = "0.2.5" +description = "Command line interface for MCP clients with secure execution and customizable security policies" +readme = "README.md" +requires-python = ">=3.10" +dependencies = ["mcp>=1.10.1"] +authors = [ + { name = "Mladen", email = "fangs-lever6n@icloud.com" }, +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project.scripts] +cli-mcp-server = "cli_mcp_server:main" + +[project.urls] +Homepage = "https://github.com/MladenSU/cli-mcp-server" +Documentation = "https://github.com/MladenSU/cli-mcp-server#readme" +Repository = "https://github.com/MladenSU/cli-mcp-server.git" +"Bug Tracker" = "https://github.com/MladenSU/cli-mcp-server/issues" \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/__init__.py new file mode 100644 index 00000000..04b915eb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/__init__.py @@ -0,0 +1,12 @@ +import asyncio + +from . import server + + +def main(): + """Main entry point for the package.""" + asyncio.run(server.main()) + + +# Optionally expose other important items at package level +__all__ = ["main", "server"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/server.py new file mode 100644 index 00000000..38a219d1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/server.py @@ -0,0 +1,690 @@ +import os +import re +import shlex +import subprocess +from dataclasses import dataclass +from typing import List, Dict, Any, Optional + +import mcp.server.stdio +import mcp.types as types +from mcp.server import NotificationOptions, Server +from mcp.server.models import InitializationOptions + +server = Server("cli-mcp-server") + + +class CommandError(Exception): + """Base exception for command-related errors""" + + pass + + +class CommandSecurityError(CommandError): + """Security violation errors""" + + pass + + +class CommandExecutionError(CommandError): + """Command execution errors""" + + pass + + +class CommandTimeoutError(CommandError): + """Command timeout errors""" + + pass + + +@dataclass +class SecurityConfig: + """ + Security configuration for command execution + """ + + allowed_commands: set[str] + allowed_flags: set[str] + max_command_length: int + command_timeout: int + allow_all_commands: bool = False + allow_all_flags: bool = False + allow_shell_operators: bool = False + max_output_length: int = 10240 + max_stdout_length: int = 8192 + max_stderr_length: int = 2048 + proxy_url: Optional[str] = None + proxy_enabled: bool = False + truncate_message: str = "...[output truncated]" + + +class CommandExecutor: + def __init__(self, allowed_dir: str, security_config: SecurityConfig): + if not allowed_dir or not os.path.exists(allowed_dir): + raise ValueError("Valid ALLOWED_DIR is required") + self.allowed_dir = os.path.abspath(os.path.realpath(allowed_dir)) + self.security_config = security_config + + def _truncate_output(self, text: str, max_length: int) -> str: + """ + Truncates output text if it exceeds the maximum length. + + Args: + text (str): The text to potentially truncate + max_length (int): Maximum allowed length + + Returns: + str: Original text if within limit, or truncated text with message + """ + if not text or len(text) <= max_length: + return text + return text[:max_length] + self.security_config.truncate_message + + def _normalize_path(self, path: str) -> str: + """ + Normalizes a path and ensures it's within allowed directory. + """ + try: + if os.path.isabs(path): + # If absolute path, check directly + real_path = os.path.abspath(os.path.realpath(path)) + else: + # If relative path, combine with allowed_dir first + real_path = os.path.abspath( + os.path.realpath(os.path.join(self.allowed_dir, path)) + ) + + if not self._is_path_safe(real_path): + raise CommandSecurityError( + f"Path '{path}' is outside of allowed directory: {self.allowed_dir}" + ) + + return real_path + except CommandSecurityError: + raise + except Exception as e: + raise CommandSecurityError(f"Invalid path '{path}': {str(e)}") + + def validate_command(self, command_string: str) -> tuple[str, List[str]]: + """ + Validates and parses a command string for security and formatting. + + Checks if the command string contains shell operators. If it does, splits the command + by operators and validates each part individually. If all parts are valid, returns + the original command string to be executed with shell=True. + + For commands without shell operators, splits into command and arguments and validates + each part according to security rules. + + Args: + command_string (str): The command string to validate and parse. + + Returns: + tuple[str, List[str]]: A tuple containing: + - For regular commands: The command name (str) and list of arguments (List[str]) + - For commands with shell operators: The full command string and empty args list + + Raises: + CommandSecurityError: If any part of the command fails security validation. + """ + + # Define shell operators + shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"] + + # Check if command contains shell operators + contains_shell_operator = any( + operator in command_string for operator in shell_operators + ) + + if contains_shell_operator: + # Check if shell operators are allowed + if not self.security_config.allow_shell_operators: + # If shell operators are not allowed, raise an error + for operator in shell_operators: + if operator in command_string: + raise CommandSecurityError( + f"Shell operator '{operator}' is not supported. Set ALLOW_SHELL_OPERATORS=true to enable." + ) + + # Split the command by shell operators and validate each part + return self._validate_command_with_operators( + command_string, shell_operators + ) + + # Process single command without shell operators + return self._validate_single_command(command_string) + + def _is_url_path(self, path: str) -> bool: + """ + Checks if a given path is a URL of type http or https. + + Args: + path (str): The path to check. + + Returns: + bool: True if the path is a URL, False otherwise. + """ + url_pattern = re.compile(r"^https?://") + return bool(url_pattern.match(path)) + + def _is_path_safe(self, path: str) -> bool: + """ + Checks if a given path is safe to access within allowed directory boundaries. + + Validates that the absolute resolved path is within the allowed directory + to prevent directory traversal attacks. + + Args: + path (str): The path to validate. + + Returns: + bool: True if path is within allowed directory, False otherwise. + Returns False if path resolution fails for any reason. + + Private method intended for internal use only. + """ + try: + # Resolve any symlinks and get absolute path + real_path = os.path.abspath(os.path.realpath(path)) + allowed_dir_real = os.path.abspath(os.path.realpath(self.allowed_dir)) + + # Check if the path starts with allowed_dir + return real_path.startswith(allowed_dir_real) + except Exception: + return False + + def _validate_single_command(self, command_string: str) -> tuple[str, List[str]]: + """ + Validates a single command without shell operators. + + Args: + command_string (str): The command string to validate. + + Returns: + tuple[str, List[str]]: A tuple containing the command and validated arguments. + + Raises: + CommandSecurityError: If the command fails validation. + """ + try: + parts = shlex.split(command_string) + if not parts: + raise CommandSecurityError("Empty command") + + command, args = parts[0], parts[1:] + + # Validate command if not in allow-all mode + if ( + not self.security_config.allow_all_commands + and command not in self.security_config.allowed_commands + ): + raise CommandSecurityError(f"Command '{command}' is not allowed") + + # Process and validate arguments + validated_args = [] + for arg in args: + is_explicit_path = (arg.startswith(("./", "../", "/")) and not arg.startswith("//")) or arg == "." + + if arg.startswith("-"): + if ( + not self.security_config.allow_all_flags + and arg not in self.security_config.allowed_flags + ): + raise CommandSecurityError(f"Flag '{arg}' is not allowed") + validated_args.append(arg) + continue + # For any path-like argument, validate it + if is_explicit_path or ("/" in arg and os.path.exists(os.path.join(self.allowed_dir, arg))): + if self._is_url_path(arg): + # If it's a URL, we don't need to normalize it + validated_args.append(arg) + continue + + normalized_path = self._normalize_path(arg) + validated_args.append(normalized_path) + else: + # For non-path arguments, add them as-is + validated_args.append(arg) + + return command, validated_args + + except ValueError as e: + raise CommandSecurityError(f"Invalid command format: {str(e)}") + + def _validate_command_with_operators( + self, command_string: str, shell_operators: List[str] + ) -> tuple[str, List[str]]: + """ + Validates a command string that contains shell operators. + + FIXED VERSION: Properly handles redirection operators by understanding shell syntax context. + Filenames after redirection operators (>, >>, <, <<) are not validated as commands. + + Args: + command_string (str): The command string containing shell operators. + shell_operators (List[str]): List of shell operators to split by. + + Returns: + tuple[str, List[str]]: A tuple containing the command and empty args list + (since the command will be executed with shell=True) + + Raises: + CommandSecurityError: If any part of the command fails validation. + """ + # Define redirection operators that take filenames as arguments + redirection_operators = [">", ">>", "<", "<<"] + command_separators = ["&&", "||", "|", ";"] + + # Create a regex pattern to split by any of the shell operators + # We need to escape special regex characters in the operators + escaped_operators = [re.escape(op) for op in shell_operators] + pattern = "|".join(escaped_operators) + + # Split the command string by shell operators, keeping the operators + parts = re.split(f"({pattern})", command_string) + + # Filter out empty parts and whitespace-only parts + parts = [part.strip() for part in parts if part.strip()] + + # Parse commands with context awareness + i = 0 + while i < len(parts): + current_part = parts[i] + + # Skip if this part is an operator + if current_part in shell_operators: + i += 1 + continue + + # Check if this part should be treated as a command or a filename + is_filename_context = False + + # Look at the previous operator to determine context + if i > 0: + prev_operator = parts[i - 1] + if prev_operator in redirection_operators: + # This part is a filename after a redirection operator + is_filename_context = True + + # Validate only if this is a command, not a filename + if not is_filename_context: + try: + # This should be a command - validate it + self._validate_single_command(current_part) + except CommandSecurityError as e: + raise CommandSecurityError(f"Invalid command part '{current_part}': {str(e)}") + except ValueError as e: + raise CommandSecurityError( + f"Invalid command format in '{current_part}': {str(e)}" + ) + else: + # This is a filename after redirection - just check if it's a safe path + if not self._is_url_path(current_part): + try: + # Validate that the filename path is safe (within allowed directory) + self._normalize_path(current_part) + except CommandSecurityError as e: + raise CommandSecurityError(f"Invalid file path '{current_part}': {str(e)}") + + i += 1 + + # If we get here, all parts passed validation + # Return the original command string to be executed with shell=True + return command_string, [] + + def execute(self, command_string: str) -> subprocess.CompletedProcess: + """ + Executes a command string in a secure, controlled environment. + + Runs the command after validating it against security constraints including length limits + and shell operator restrictions. Executes with controlled parameters for safety. + + Args: + command_string (str): The command string to execute. + + Returns: + subprocess.CompletedProcess: The result of the command execution containing + stdout, stderr, and return code. + + Raises: + CommandSecurityError: If the command: + - Exceeds maximum length + - Fails security validation + - Fails during execution + + Notes: + - Uses shell=True for commands with shell operators, shell=False otherwise + - Uses timeout and working directory constraints + - Captures both stdout and stderr + """ + if len(command_string) > self.security_config.max_command_length: + raise CommandSecurityError( + f"Command exceeds maximum length of {self.security_config.max_command_length}" + ) + + try: + command, args = self.validate_command(command_string) + + # Prepare environment variables for proxy support + env = os.environ.copy() + if self.security_config.proxy_enabled and self.security_config.proxy_url: + env.update({ + 'HTTP_PROXY': self.security_config.proxy_url, + 'HTTPS_PROXY': self.security_config.proxy_url, + 'http_proxy': self.security_config.proxy_url, + 'https_proxy': self.security_config.proxy_url, + }) + + # Check if this is a command with shell operators + shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"] + use_shell = any(operator in command_string for operator in shell_operators) + + # Double-check that shell operators are allowed if they are present + if use_shell and not self.security_config.allow_shell_operators: + for operator in shell_operators: + if operator in command_string: + raise CommandSecurityError( + f"Shell operator '{operator}' is not supported. Set ALLOW_SHELL_OPERATORS=true to enable." + ) + + if use_shell: + # For commands with shell operators, execute with shell=True + return subprocess.run( + command, # command is the full command string in this case + shell=True, + text=True, + capture_output=True, + timeout=self.security_config.command_timeout, + cwd=self.allowed_dir, + env=env, + ) + else: + # For regular commands, execute with shell=False + return subprocess.run( + [command] + args, + shell=False, + text=True, + capture_output=True, + timeout=self.security_config.command_timeout, + cwd=self.allowed_dir, + env=env, + ) + except subprocess.TimeoutExpired: + raise CommandTimeoutError( + f"Command timed out after {self.security_config.command_timeout} seconds" + ) + except CommandError: + raise + except Exception as e: + raise CommandExecutionError(f"Command execution failed: {str(e)}") + + +# Load security configuration from environment +def load_security_config() -> SecurityConfig: + """ + Loads security configuration from environment variables with default fallbacks. + + Creates a SecurityConfig instance using environment variables to configure allowed + commands, flags, patterns, and execution constraints. Uses predefined defaults if + environment variables are not set. + + Returns: + SecurityConfig: Configuration object containing: + - allowed_commands: Set of permitted command names + - allowed_flags: Set of permitted command flags/options + - max_command_length: Maximum length of command string + - command_timeout: Maximum execution time in seconds + - allow_all_commands: Whether all commands are allowed + - allow_all_flags: Whether all flags are allowed + - allow_shell_operators: Whether shell operators (&&, ||, |, etc.) are allowed + - max_output_length: Maximum total output length + - max_stdout_length: Maximum stdout length + - max_stderr_length: Maximum stderr length + - proxy_url: Proxy URL for HTTP/HTTPS requests + - proxy_enabled: Whether proxy is enabled + - truncate_message: Message shown when output is truncated + + Environment Variables: + ALLOWED_COMMANDS: Comma-separated list of allowed commands or 'all' (default: "ls,cat,pwd") + ALLOWED_FLAGS: Comma-separated list of allowed flags or 'all' (default: "-l,-a,--help") + MAX_COMMAND_LENGTH: Maximum command string length (default: 1024) + COMMAND_TIMEOUT: Command timeout in seconds (default: 30) + ALLOW_SHELL_OPERATORS: Whether to allow shell operators like &&, ||, |, >, etc. (default: false) + Set to "true" or "1" to enable, any other value to disable. + MAX_OUTPUT_LENGTH: Maximum total output length (default: 10240) + MAX_STDOUT_LENGTH: Maximum stdout length (default: 8192) + MAX_STDERR_LENGTH: Maximum stderr length (default: 2048) + CLI_PROXY_ENABLED: Enable proxy support (default: false) + CLI_PROXY_URL: Proxy URL (also checks HTTP_PROXY if not set) + OUTPUT_TRUNCATE_MESSAGE: Message shown when output is truncated (default: "...[output truncated]") + """ + allowed_commands = os.getenv("ALLOWED_COMMANDS", "ls,cat,pwd") + allowed_flags = os.getenv("ALLOWED_FLAGS", "-l,-a,--help") + allow_shell_operators_env = os.getenv("ALLOW_SHELL_OPERATORS", "false") + + allow_all_commands = allowed_commands.lower() == "all" + allow_all_flags = allowed_flags.lower() == "all" + allow_shell_operators = allow_shell_operators_env.lower() in ("true", "1") + + # Proxy configuration + proxy_url = os.getenv("CLI_PROXY_URL") or os.getenv("HTTP_PROXY") + proxy_enabled = os.getenv("CLI_PROXY_ENABLED", "false").lower() in ("true", "1") + + # Output length limits with error handling + try: + max_command_length = int(os.getenv("MAX_COMMAND_LENGTH", "1024")) + if max_command_length <= 0: + max_command_length = 1024 + except ValueError: + max_command_length = 1024 + + try: + command_timeout = int(os.getenv("COMMAND_TIMEOUT", "30")) + if command_timeout <= 0: + command_timeout = 30 + except ValueError: + command_timeout = 30 + + try: + max_output_length = int(os.getenv("MAX_OUTPUT_LENGTH", "10240")) + if max_output_length <= 0: + max_output_length = 10240 + except ValueError: + max_output_length = 10240 + + try: + max_stdout_length = int(os.getenv("MAX_STDOUT_LENGTH", "8192")) + if max_stdout_length <= 0: + max_stdout_length = 8192 + except ValueError: + max_stdout_length = 8192 + + try: + max_stderr_length = int(os.getenv("MAX_STDERR_LENGTH", "2048")) + if max_stderr_length <= 0: + max_stderr_length = 2048 + except ValueError: + max_stderr_length = 2048 + + return SecurityConfig( + allowed_commands=( + set() if allow_all_commands else set(allowed_commands.split(",")) + ), + allowed_flags=set() if allow_all_flags else set(allowed_flags.split(",")), + max_command_length=max_command_length, + command_timeout=command_timeout, + allow_all_commands=allow_all_commands, + allow_all_flags=allow_all_flags, + allow_shell_operators=allow_shell_operators, + max_output_length=max_output_length, + max_stdout_length=max_stdout_length, + max_stderr_length=max_stderr_length, + proxy_url=proxy_url, + proxy_enabled=proxy_enabled, + truncate_message=os.getenv("OUTPUT_TRUNCATE_MESSAGE", "...[output truncated]"), + ) + + +executor = CommandExecutor( + allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config() +) + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + commands_desc = ( + "all commands" + if executor.security_config.allow_all_commands + else ", ".join(executor.security_config.allowed_commands) + ) + flags_desc = ( + "all flags" + if executor.security_config.allow_all_flags + else ", ".join(executor.security_config.allowed_flags) + ) + + return [ + types.Tool( + name="run_command", + description=( + f"Allows command (CLI) execution in the directory: {executor.allowed_dir}\n\n" + f"Available commands: {commands_desc}\n" + f"Available flags: {flags_desc}\n\n" + f"Shell operators (&&, ||, |, >, >>, <, <<, ;) are {'supported' if executor.security_config.allow_shell_operators else 'not supported'}. Set ALLOW_SHELL_OPERATORS=true to enable." + ), + inputSchema={ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Single command to execute (example: 'ls -l' or 'cat file.txt')", + } + }, + "required": ["command"], + }, + ), + types.Tool( + name="show_security_rules", + description=( + "Show what commands and operations are allowed in this environment.\n" + ), + inputSchema={ + "type": "object", + "properties": {}, + }, + ), + ] + + +@server.call_tool() +async def handle_call_tool( + name: str, arguments: Optional[Dict[str, Any]] +) -> List[types.TextContent]: + if name == "run_command": + if not arguments or "command" not in arguments: + return [ + types.TextContent(type="text", text="No command provided", error=True) + ] + + try: + result = executor.execute(arguments["command"]) + + response = [] + if result.stdout: + stdout_truncated = executor._truncate_output( + result.stdout, + executor.security_config.max_stdout_length + ) + response.append(types.TextContent(type="text", text=stdout_truncated)) + + if result.stderr: + stderr_truncated = executor._truncate_output( + result.stderr, + executor.security_config.max_stderr_length + ) + response.append( + types.TextContent(type="text", text=stderr_truncated, error=True) + ) + + response.append( + types.TextContent( + type="text", + text=f"\nCommand completed with return code: {result.returncode}", + ) + ) + + return response + + except CommandSecurityError as e: + return [ + types.TextContent( + type="text", text=f"Security violation: {str(e)}", error=True + ) + ] + except subprocess.TimeoutExpired: + return [ + types.TextContent( + type="text", + text=f"Command timed out after {executor.security_config.command_timeout} seconds", + error=True, + ) + ] + except Exception as e: + return [types.TextContent(type="text", text=f"Error: {str(e)}", error=True)] + + elif name == "show_security_rules": + commands_desc = ( + "All commands allowed" + if executor.security_config.allow_all_commands + else ", ".join(sorted(executor.security_config.allowed_commands)) + ) + flags_desc = ( + "All flags allowed" + if executor.security_config.allow_all_flags + else ", ".join(sorted(executor.security_config.allowed_flags)) + ) + + security_info = ( + "Security Configuration:\n" + f"==================\n" + f"Working Directory: {executor.allowed_dir}\n" + f"\nAllowed Commands:\n" + f"----------------\n" + f"{commands_desc}\n" + f"\nAllowed Flags:\n" + f"-------------\n" + f"{flags_desc}\n" + f"\nSecurity Limits:\n" + f"---------------\n" + f"Max Command Length: {executor.security_config.max_command_length} characters\n" + f"Command Timeout: {executor.security_config.command_timeout} seconds\n" + f"Max Stdout Length: {executor.security_config.max_stdout_length} characters\n" + f"Max Stderr Length: {executor.security_config.max_stderr_length} characters\n" + f"Shell Operators: {'Enabled' if executor.security_config.allow_shell_operators else 'Disabled'}\n" + f"\nProxy Configuration:\n" + f"-------------------\n" + f"Proxy Enabled: {'Yes' if executor.security_config.proxy_enabled else 'No'}\n" + f"Proxy URL: {executor.security_config.proxy_url or 'Not configured'}\n" + f"\nOutput Settings:\n" + f"---------------\n" + f"Truncate Message: {executor.security_config.truncate_message}\n" + ) + return [types.TextContent(type="text", text=security_info)] + + raise ValueError(f"Unknown tool: {name}") + + +async def main(): + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="cli-mcp-server", + server_version="0.2.1", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/__init__.py new file mode 100644 index 00000000..65140f2e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/test_cli_mcp_server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/test_cli_mcp_server.py new file mode 100644 index 00000000..be6e4dd7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/test_cli_mcp_server.py @@ -0,0 +1,224 @@ +import os +import importlib +import asyncio +import shutil +import tempfile +import unittest + + +# Helper to print results in a simple table format +def print_results_table(name: str, results: list) -> None: + print(f"\n[{name}] Results Table:") + print("Idx | Type | Error | Text") + print("-----|--------|-------|-----") + for idx, tc in enumerate(results): + error_flag = getattr(tc, "error", False) + # Replace newlines in text for single-line display + text = tc.text.strip().replace("\n", "\\n") + print(f"{idx:<3} | {tc.type:<6} | {error_flag!s:<5} | {text}") + + +class TestCLIMCPServer(unittest.TestCase): + def setUp(self): + # Create a temporary directory for allowed_dir + self.tempdir = tempfile.TemporaryDirectory() + os.environ["ALLOWED_DIR"] = self.tempdir.name + # Remove custom allowed commands/flags to use defaults + os.environ.pop("ALLOWED_COMMANDS", None) + os.environ.pop("ALLOWED_FLAGS", None) + # Ensure shell operators are disabled by default + os.environ.pop("ALLOW_SHELL_OPERATORS", None) + # Reload server module to pick up env changes + try: + import cli_mcp_server.server as server_module + + self.server = importlib.reload(server_module) + except ImportError: + import cli_mcp_server.server as server_module + + self.server = server_module + + def tearDown(self): + self.tempdir.cleanup() + + def test_run_pwd(self): + # Run 'pwd' command + result = asyncio.run( + self.server.handle_call_tool("run_command", {"command": "pwd"}) + ) + texts = [tc.text for tc in result] + # Debug print: show results in table form + print_results_table("test_run_pwd", result) + self.assertTrue(texts, "No output returned") + self.assertEqual(texts[0].strip(), self.tempdir.name) + self.assertTrue(any("return code: 0" in text for text in texts)) + + def test_run_ls(self): + # Create a file in the allowed directory + file_path = os.path.join(self.tempdir.name, "foo.txt") + with open(file_path, "w") as f: + f.write("test") + result = asyncio.run( + self.server.handle_call_tool("run_command", {"command": "ls"}) + ) + texts = [tc.text for tc in result] + # Debug print: show results in table form + print_results_table("test_run_ls", result) + self.assertTrue( + any("foo.txt" in text for text in texts), + f"Output did not contain 'foo.txt': {texts}", + ) + self.assertTrue(any("return code: 0" in text for text in texts)) + + def test_run_curl_ifconfig(self): + # Skip test if curl is not available + if not shutil.which("curl"): + self.skipTest("curl is not available on PATH") + # Allow all commands and flags + os.environ["ALLOWED_COMMANDS"] = "all" + os.environ["ALLOWED_FLAGS"] = "all" + # Reload server to pick up new settings + import cli_mcp_server.server as server_module + + self.server = importlib.reload(server_module) + result = asyncio.run( + self.server.handle_call_tool( + "run_command", {"command": "curl -sG ifconfig.me"} + ) + ) + texts = [tc.text for tc in result] + # Debug print: show results in table form + print_results_table("test_run_curl_ifconfig", result) + output_texts = [t for t in texts if "return code" not in t] + self.assertTrue( + any(t.strip() for t in output_texts), f"No IP address retrieved: {texts}" + ) + self.assertTrue(any("return code: 0" in text for text in texts)) + + def test_shell_operator_disallowed(self): + # Ensure shell operators are disabled by default + result = asyncio.run( + self.server.handle_call_tool("run_command", {"command": "echo 1 && echo 2"}) + ) + texts = [tc.text for tc in result] + print_results_table("test_shell_operator_disallowed", result) + self.assertTrue( + any("Security violation" in text for text in texts), + f"Expected security violation for shell operators, got: {texts}", + ) + self.assertTrue( + any("Shell operator '&&' is not supported" in text for text in texts), + f"Expected '&&' not supported message, got: {texts}", + ) + + def test_shell_operator_allowed_and_executes_commands(self): + # Enable shell operators and allow all commands/flags + os.environ["ALLOW_SHELL_OPERATORS"] = "true" + os.environ["ALLOWED_COMMANDS"] = "all" + os.environ["ALLOWED_FLAGS"] = "all" + # Reload server to pick up new settings + import cli_mcp_server.server as server_module + + server = importlib.reload(server_module) + # Execute a compound command with '&&' + result = asyncio.run( + server.handle_call_tool("run_command", {"command": "echo 3 && echo 4"}) + ) + texts = [tc.text for tc in result] + print_results_table("test_shell_operator_allowed", result) + # The first element should contain the combined stdout from both commands + self.assertEqual( + texts[0].strip(), + "3\n4", + f"Unexpected combined output, got: {texts[0]!r}", + ) + self.assertTrue(any("return code: 0" in text for text in texts)) + + def test_shell_operator_semicolon(self): + # Enable shell operators and allow all commands/flags + os.environ["ALLOW_SHELL_OPERATORS"] = "true" + os.environ["ALLOWED_COMMANDS"] = "all" + os.environ["ALLOWED_FLAGS"] = "all" + # Reload server to pick up new settings + import cli_mcp_server.server as server_module + + server = importlib.reload(server_module) + # Execute a compound command with ';' + result = asyncio.run( + server.handle_call_tool("run_command", {"command": "echo 5; echo 6"}) + ) + texts = [tc.text for tc in result] + print_results_table("test_shell_operator_semicolon", result) + self.assertEqual( + texts[0].strip(), + "5\n6", + f"Unexpected combined output, got: {texts[0]!r}", + ) + self.assertTrue(any("return code: 0" in text for text in texts)) + + def test_shell_operator_append_redirection(self): + # Enable shell operators and allow all commands/flags + os.environ["ALLOW_SHELL_OPERATORS"] = "true" + os.environ["ALLOWED_COMMANDS"] = "all" + os.environ["ALLOWED_FLAGS"] = "all" + # Reload server to pick up new settings + import cli_mcp_server.server as server_module + + server = importlib.reload(server_module) + # Create an output file and append text using '>>' + file_name = "append.txt" + file_path = os.path.join(self.tempdir.name, file_name) + # Ensure the file exists + open(file_path, "w").close() + result = asyncio.run( + server.handle_call_tool("run_command", {"command": f"echo hello >> {file_name}"}) + ) + texts = [tc.text for tc in result] + print_results_table("test_shell_operator_append_redirection", result) + # After redirection, file should contain 'hello' + with open(file_path, "r") as f: + content = f.read().strip() + self.assertEqual(content, "hello", f"Unexpected file content: {content!r}") + self.assertTrue(any("return code: 0" in text for text in texts)) + + def test_shell_operator_pipe(self): + # Enable shell operators and allow all commands/flags + os.environ["ALLOW_SHELL_OPERATORS"] = "true" + os.environ["ALLOWED_COMMANDS"] = "all" + os.environ["ALLOWED_FLAGS"] = "all" + # Reload server to pick up new settings + import cli_mcp_server.server as server_module + + server = importlib.reload(server_module) + # Execute a simple pipeline to filter output + result = asyncio.run( + server.handle_call_tool("run_command", {"command": "echo 123 | grep 123"}) + ) + texts = [tc.text for tc in result] + print_results_table("test_shell_operator_pipe", result) + # The pipeline should output '123' + self.assertEqual(texts[0].strip(), "123", f"Unexpected pipeline output: {texts[0]!r}") + self.assertTrue(any("return code: 0" in text for text in texts)) + + def test_shell_operator_or(self): + # Enable shell operators and allow all commands/flags + os.environ["ALLOW_SHELL_OPERATORS"] = "true" + os.environ["ALLOWED_COMMANDS"] = "all" + os.environ["ALLOWED_FLAGS"] = "all" + # Reload server to pick up new settings + import cli_mcp_server.server as server_module + + server = importlib.reload(server_module) + # Use '||' to fallback on failure + result = asyncio.run( + server.handle_call_tool("run_command", {"command": "false || echo OR_OK"}) + ) + texts = [tc.text for tc in result] + print_results_table("test_shell_operator_or", result) + # The OR operation should output 'OR_OK' + self.assertEqual(texts[0].strip(), "OR_OK", f"Unexpected OR output: {texts[0]!r}") + self.assertTrue(any("return code: 0" in text for text in texts)) + + +if __name__ == "__main__": + unittest.main() diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/uv.lock new file mode 100644 index 00000000..d1b16810 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/uv.lock @@ -0,0 +1,532 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cli-mcp-server" +version = "0.2.5" +source = { editable = "." } +dependencies = [ + { name = "mcp" }, +] + +[package.metadata] +requires-dist = [{ name = "mcp", specifier = ">=1.10.1" }] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { 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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, +] + +[[package]] +name = "mcp" +version = "1.10.1" +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 = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/68/63045305f29ff680a9cd5be360c755270109e6b76f696ea6824547ddbc30/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2", size = 392969 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/3f/435a5b3d10ae242a9d6c2b33175551173c3c61fe637dc893be05c4ed0aaf/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5", size = 150878 }, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, + { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, + { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, + { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, + { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, + { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, + { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, + { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "rpds-py" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466 }, + { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530 }, + { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933 }, + { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973 }, + { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293 }, + { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787 }, + { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312 }, + { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403 }, + { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323 }, + { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541 }, + { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442 }, + { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314 }, + { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610 }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032 }, + { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525 }, + { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089 }, + { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255 }, + { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283 }, + { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881 }, + { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822 }, + { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347 }, + { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956 }, + { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363 }, + { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123 }, + { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732 }, + { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917 }, + { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933 }, + { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447 }, + { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711 }, + { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865 }, + { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763 }, + { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651 }, + { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079 }, + { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379 }, + { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033 }, + { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639 }, + { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105 }, + { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272 }, + { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995 }, + { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198 }, + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917 }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073 }, + { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214 }, + { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113 }, + { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189 }, + { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998 }, + { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903 }, + { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785 }, + { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329 }, + { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875 }, + { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636 }, + { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663 }, + { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428 }, + { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571 }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475 }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692 }, + { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415 }, + { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783 }, + { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844 }, + { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105 }, + { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440 }, + { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759 }, + { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032 }, + { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416 }, + { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049 }, + { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428 }, + { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524 }, + { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292 }, + { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334 }, + { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875 }, + { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993 }, + { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683 }, + { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825 }, + { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292 }, + { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435 }, + { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410 }, + { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724 }, + { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285 }, + { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459 }, + { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083 }, + { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291 }, + { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445 }, + { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206 }, + { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330 }, + { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254 }, + { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094 }, + { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889 }, + { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301 }, + { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891 }, + { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044 }, + { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774 }, + { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886 }, + { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027 }, + { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821 }, + { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226 }, + { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230 }, + { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363 }, + { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146 }, + { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804 }, + { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820 }, + { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567 }, + { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520 }, + { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362 }, + { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113 }, + { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429 }, + { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950 }, + { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505 }, + { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468 }, + { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680 }, + { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035 }, + { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922 }, + { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822 }, + { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336 }, + { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871 }, + { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439 }, + { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380 }, + { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sse-starlette" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 }, +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.gitignore new file mode 100644 index 00000000..505a3b1c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/LICENSE new file mode 100644 index 00000000..29dd7ce9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 lockon-n + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/README.md new file mode 100644 index 00000000..aa54eb00 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/README.md @@ -0,0 +1,369 @@ +# Emails MCP Server + +A FastMCP-based email management server for IMAP/SMTP operations through the Model Context Protocol (MCP). + +## Features + +- 📧 **Email Management**: Get, read, search emails with pagination support +- 📤 **Send & Reply**: Send emails with HTML/text, attachments, CC/BCC support +- 📁 **Folder Operations**: List, create, delete email folders +- 📋 **Draft Management**: Save, update, delete, and manage email drafts +- 📦 **Import/Export**: Backup/restore emails with folder structure preservation +- 🔗 **Attachment Handling**: Download email attachments to specified locations +- 📊 **Statistics**: Get mailbox statistics and unread counts +- 🛡️ **Security**: Workspace isolation and secure IMAP/SMTP connections + +## Installation + +### Uv is recommanded + +```bash +uv tool install emails-mcp +``` + +### From source + +```bash +git clone https://github.com/lockon-n/emails-mcp.git +cd emails-mcp +uv sync +``` + +## Usage + +### Configuration + +Create email configuration file (`/path/to/your/email/config.json`): + +**Single account format:** +```json +{ + "email": "your-email@example.com", + "password": "your-password", + "name": "Your Name", + "imap_server": "imap.example.com", + "imap_port": yourport (typically 993), + "smtp_server": "smtp.example.com", + "smtp_port": yourport (typically 587), + "use_ssl": true/false, + "use_starttls": true/false +} +``` + +### Usage with Claude Desktop + +Add to your `~/.config/claude/claude_desktop_config.json` (Linux/macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): + +**Published Configuration:** +```json +{ + "mcpServers": { + "emails-mcp": { + "command": "uvx", + "args": [ + "emails-mcp", + "--config_file", + "", + "--attachment_upload_path", + "", + "--attachment_download_path", + "", + "--email_export_path", + "" + ] + } + } +} +``` + +**Development/Unpublished Configuration:** +```json +{ + "mcpServers": { + "emails-mcp": { + "command": "uv", + "args": [ + "--directory", + "", + "run", + "emails-mcp", + "--config_file", + "", + "--attachment_upload_path", + "", + "--attachment_download_path", + "", + "--email_export_path", + "" + ] + } + } +} +``` + +**Note**: For security, this tool restricts file operations to the specified directories: +- `--attachment_upload_path`: Restricts attachment file selection +- `--attachment_download_path`: Where downloaded attachments are saved +- `--email_export_path`: Where email exports are saved + +### As a command line tool + +```bash +# Basic usage (uses default config file: test_emils.json) +emails-mcp + +# With custom configuration file +emails-mcp --config_file config.json + +# With path restrictions for security +emails-mcp --config_file config.json \ + --attachment_download_path ./downloads \ + --email_export_path ./exports \ + --attachment_upload_path ./uploads + +# With debug logging +emails-mcp --config_file config.json --debug +``` + +#### Supported Arguments + +- `--config_file`: Email configuration file path +- `--attachment_upload_path`: Directory for attachment uploads (restricts file selection) +- `--attachment_download_path`: Directory for attachment downloads (files saved here) +- `--email_export_path`: Directory for email exports (exports saved here) +- `--debug`: Enable debug logging + +## Available Tools + +
+📧 Email Operations + +### get_emails +Get paginated list of emails from specified folder +- `folder`: Email folder name (default: "INBOX") +- `page`: Page number starting from 1 (default: 1) +- `page_size`: Number of emails per page (default: 20) + +### read_email +Read full content of a specific email +- `email_id`: Email ID to read + +### search_emails +Search emails with query string (sorted by date descending) +- `query`: Search query (subject, from, body content) +- `folder`: Folder to search in (optional) +- `page`: Page number starting from 1 (default: 1) +- `page_size`: Number of results per page (default: 20) + +### send_email +Send an email with optional HTML body, CC, BCC, and attachments +- `to`: Recipient email address(es), comma-separated +- `subject`: Email subject +- `body`: Plain text body +- `html_body`: HTML body content (optional) +- `cc`: CC recipients, comma-separated (optional) +- `bcc`: BCC recipients, comma-separated (optional) +- `attachments`: List of file paths to attach (optional) + +### reply_email +Reply to an email +- `email_id`: ID of email to reply to +- `body`: Reply message body (plain text) +- `html_body`: Reply message body (HTML, optional) +- `cc`: Additional CC recipients (optional) +- `bcc`: BCC recipients (optional) +- `reply_all`: Whether to reply to all recipients (default: False) + +### forward_email +Forward an email to other recipients +- `email_id`: ID of email to forward +- `to`: Recipients to forward to +- `body`: Additional message body (optional) +- `html_body`: Additional HTML message body (optional) +- `cc`: CC recipients (optional) +- `bcc`: BCC recipients (optional) + +### delete_email / delete_emails +Delete single or multiple emails +- `email_id`: Email ID to delete (single) +- `email_ids`: List of email IDs to delete (batch) + +### move_email / move_emails +Move single or multiple emails to another folder +- `email_id`: Email ID to move (single) +- `email_ids`: List of email IDs to move (batch) +- `target_folder`: Target folder name + +### mark_emails +Mark multiple emails with status +- `email_ids`: List of email IDs to mark +- `status`: Status to set (read, unread, important, not_important) + +
+ +
+📁 Folder Operations + +### get_folders +Get list of available email folders + +### create_folder +Create new email folder +- `folder_name`: Name of folder to create + +### delete_folder +Delete email folder +- `folder_name`: Name of folder to delete + +### get_mailbox_stats +Get mailbox statistics +- `folder_name`: Specific folder name (optional, defaults to all folders) + +### get_unread_count +Get unread message count +- `folder_name`: Specific folder name (optional, defaults to all folders) + +
+ +
+📋 Draft Management + +### save_draft +Save email draft +- `subject`: Email subject +- `body`: Plain text body +- `html_body`: HTML body content (optional) +- `to`: Recipient email address(es) (optional) +- `cc`: CC recipients (optional) +- `bcc`: BCC recipients (optional) + +### get_drafts +Get list of saved drafts +- `page`: Page number starting from 1 (default: 1) +- `page_size`: Number of drafts per page (default: 20) + +### update_draft +Update existing draft +- `draft_id`: Draft ID to update +- `subject`: Email subject (optional) +- `body`: Plain text body (optional) +- `html_body`: HTML body content (optional) +- `to`: Recipient email address(es) (optional) +- `cc`: CC recipients (optional) +- `bcc`: BCC recipients (optional) + +### delete_draft +Delete draft +- `draft_id`: Draft ID to delete + +
+ +
+🔧 Management & Utilities + +### check_connection +Check email server connection status + +### get_email_headers +Get complete email headers for technical analysis +- `email_id`: Email ID to get headers for + +### export_emails +Export emails to file for backup +- `folder`: Specific folder to export (optional) +- `export_path`: Path where to save the export file (default: "emails_export.json") +- `max_emails`: Maximum number of emails to export (optional) +- `export_all_folders`: Export from all folders instead of just one (default: False) + +### import_emails +Import emails from backup file to IMAP server +- `import_path`: Path to import file +- `target_folder`: Target folder for imported emails (if preserve_folders=False) +- `preserve_folders`: Whether to preserve original folder structure (default: True) + +### download_attachment +Download email attachment to specified path +- `email_id`: Email ID containing the attachment +- `attachment_filename`: Name of attachment to download +- `download_path`: Directory path where to save the attachment + +
+ +## Configuration Options + +### Email Server Settings +- **IMAP/SMTP servers**: Configure your email provider's servers +- **Security**: Supports SSL/TLS and STARTTLS +- **Authentication**: Standard username/password authentication + +### Workspace Security +- **Path Restriction**: Limit file operations to specified directory +- **Path Validation**: All file paths are validated for security + +### Pagination +- **Default page size**: 20 items per page +- **Maximum page size**: 50 items per page +- **Auto-correction**: Invalid page parameters are automatically corrected + +## Error Handling + +The server includes comprehensive error handling: +- **Connection errors**: Graceful handling of network issues +- **Authentication errors**: Clear error messages for login failures +- **Validation errors**: Input validation with helpful error messages +- **File system errors**: Proper handling of file operation failures + +## Security Considerations + +- **Workspace isolation**: File operations can be restricted to a safe directory +- **Input validation**: All user inputs are validated +- **Connection security**: Supports SSL/TLS encryption +- **Error messages**: Avoid exposing sensitive information in error messages + +## Development + +### Build + +```bash +uv build +``` + +### Publish to PyPI + +```bash +uv publish +``` + +### Local Development + +```bash +# Install development dependencies +uv sync + +# Run tests +uv run python -m pytest + +# Run server +uv run python -m emails_mcp.server +``` + +### Project Structure +The codebase follows software engineering best practices: + +``` +- models/ # Data models and configurations +- config/ # Configuration management +- utils/ # Utilities, validators, and parsers +- backends/ # IMAP, SMTP, and file operation backends +- services/ # Business logic layer +- tools/ # MCP tool definitions +- server.py # Main MCP server entry point +``` + +## License + +MIT License + +## Contributing + +Issues and Pull Requests are welcome! \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/package-lock.json new file mode 100644 index 00000000..5579f75c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "emails-mcp", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/pyproject.toml new file mode 100644 index 00000000..a8deae5e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "emails-mcp" +version = "0.1.11" +description = "A FastMCP-based email management server for IMAP/SMTP operations" +readme = "README.md" +authors = [ + { name = "lockon-n", email = "lockonlvange@gmail.com" } +] +requires-python = ">=3.12" +dependencies = [ + "fastmcp>=2.10.5", + "mcp[cli]>=1.11.0", + "email-validator>=2.1.1", + "typing-extensions>=4.9.0", + "requests>=2.32.4", + "beautifulsoup4>=4.13.4", + "imapclient>=3.0.1", + "psycopg2-binary>=2.9", +] + +[project.scripts] +emails-mcp = "emails_mcp.server:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__init__.py new file mode 100644 index 00000000..dbf888d2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("Hello from emails-mcp!") diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__main__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__main__.py new file mode 100644 index 00000000..1487999f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__main__.py @@ -0,0 +1,8 @@ +""" +Entry point for running emails_mcp as a module with python -m +""" + +from .server import main + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/__init__.py new file mode 100644 index 00000000..157b94a1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/__init__.py @@ -0,0 +1,5 @@ +from .imap_backend import IMAPBackend +from .smtp_backend import SMTPBackend +from .file_backend import FileBackend + +__all__ = ['IMAPBackend', 'SMTPBackend', 'FileBackend'] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/file_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/file_backend.py new file mode 100644 index 00000000..c8950d84 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/file_backend.py @@ -0,0 +1,356 @@ +import json +import os +import base64 +from pathlib import Path +from typing import List +from datetime import datetime +from ..models.email import EmailMessage +from ..utils.exceptions import ValidationError +from ..utils.validators import validate_file_path +from ..utils.email_parser import parse_raw_email +import logging +from email import encoders + + +class FileBackend: + """File backend for email import/export operations""" + + def __init__(self, email_export_path: str = None, attachment_download_path: str = None): + self.email_export_path = Path(email_export_path) if email_export_path else None + self.attachment_download_path = Path(attachment_download_path) if attachment_download_path else None + + def export_emails(self, emails: List[EmailMessage], filename_prefix: str = "emails_export", + format: str = 'json') -> str: + """Export emails to file using configured export path with date-based filename""" + from datetime import datetime + + # Use configured export path or current directory + if self.email_export_path: + export_dir = self.email_export_path + else: + export_dir = Path.cwd() + + try: + export_dir.mkdir(parents=True, exist_ok=True) + + # Generate date-based filename + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{filename_prefix}_{timestamp}.{format.lower()}" + export_file = export_dir / filename + + if format.lower() == 'json': + self._export_to_json(emails, export_file) + elif format.lower() == 'eml': + self._export_to_eml(emails, export_file) + else: + raise ValidationError(f"Unsupported export format: {format}") + + return str(export_file) # Return the actual file path + + except Exception as e: + raise ValidationError(f"Export failed: {str(e)}") + + def import_emails(self, import_path: str) -> List[EmailMessage]: + """Import emails from file with time-based sorting (newest first)""" + + # Validate import path + valid, error = validate_file_path(import_path, must_exist=True) + if not valid: + raise ValidationError(f"Invalid import path: {error}") + + try: + import_file = Path(import_path) + + emails = [] + if import_file.suffix.lower() == '.json': + # logging.warning("Importing emails from JSON file") + emails = self._import_from_json(import_file) + elif import_file.suffix.lower() == '.eml': + # logging.warning("Importing emails from EML file") + emails = self._import_from_eml(import_file) + elif import_file.is_dir(): + emails = self._import_from_directory(import_file) + else: + raise ValidationError(f"Unsupported import format: {import_file.suffix}") + + # Sort emails by email_id (oldest first) for consistent import order + emails.sort(key=lambda email: int(email.email_id) if email.email_id.isdigit() else 0, reverse=False) + + return emails + + except Exception as e: + raise ValidationError(f"Import failed: {str(e)}") + + def _parse_email_date(self, date_str: str) -> datetime: + """Parse email date string to datetime object for sorting""" + if not date_str: + return datetime.min.replace(tzinfo=None) # Put emails without dates at the end + + try: + from email.utils import parsedate_to_datetime + parsed_date = parsedate_to_datetime(date_str) + + # Convert to naive datetime for consistent comparison + if parsed_date.tzinfo is not None: + # Convert to UTC and then remove timezone info + parsed_date = parsed_date.utctimetuple() + parsed_date = datetime(*parsed_date[:6]) + + return parsed_date + except Exception: + # Fallback to current time if parsing fails (make it naive) + return datetime.now().replace(tzinfo=None) + + def _export_to_json(self, emails: List[EmailMessage], export_file: Path): + """Export emails to JSON format""" + export_data = { + 'export_date': datetime.now().isoformat(), + 'total_emails': len(emails), + 'emails': [] + } + + for email_obj in emails: + email_data = { + 'email_id': email_obj.email_id, + 'subject': email_obj.subject, + 'from_addr': email_obj.from_addr, + 'to_addr': email_obj.to_addr, + 'cc_addr': email_obj.cc_addr, + 'bcc_addr': email_obj.bcc_addr, + 'date': email_obj.date, + 'message_id': email_obj.message_id, + 'body_text': email_obj.body_text, + 'body_html': email_obj.body_html, + 'is_read': email_obj.is_read, + 'is_important': email_obj.is_important, + 'folder': email_obj.folder, + 'attachments': [ + { + 'filename': att.filename, + 'content_type': att.content_type, + 'size': att.size, + 'content': base64.b64encode(att.content).decode('utf-8') if att.content else None + } + for att in email_obj.attachments + ] + } + export_data['emails'].append(email_data) + + with open(export_file, 'w', encoding='utf-8') as f: + json.dump(export_data, f, indent=2, ensure_ascii=False) + + def _export_to_eml(self, emails: List[EmailMessage], export_path: Path): + """Export emails to EML format (directory of .eml files)""" + export_dir = export_path + if export_path.suffix: + export_dir = export_path.parent / export_path.stem + + export_dir.mkdir(parents=True, exist_ok=True) + + for i, email_obj in enumerate(emails): + if email_obj.raw_message: + filename = f"{i+1:04d}_{email_obj.email_id}.eml" + eml_file = export_dir / filename + + with open(eml_file, 'wb') as f: + f.write(email_obj.raw_message.as_bytes()) + + def _import_from_json(self, import_file: Path) -> List[EmailMessage]: + """Import emails from JSON format""" + with open(import_file, 'r', encoding='utf-8') as f: + import_data = json.load(f) + + if 'emails' not in import_data: + raise ValidationError("Invalid JSON format: missing 'emails' key") + + emails = [] + for email_data in import_data['emails']: + try: + # Create EmailMessage from JSON data + from ..models.email import EmailAttachment + + attachments = [] + for att_data in email_data.get('attachments', []): + # 解码附件内容(如果存在) + content = None + if att_data.get('content'): + try: + content = base64.b64decode(att_data['content']) + except Exception as decode_error: # 修正:给异常一个具体的变量名 + # 如果解码失败,保持content为None + logging.debug(f"Failed to decode attachment content: {str(decode_error)}") + content = None + + attachment = EmailAttachment( + filename=att_data['filename'], + content_type=att_data['content_type'], + size=att_data['size'], + content=content + ) + attachments.append(attachment) + + + + email_obj = EmailMessage( + email_id=email_data['email_id'], + subject=email_data['subject'], + from_addr=email_data['from_addr'], + to_addr=email_data['to_addr'], + cc_addr=email_data.get('cc_addr'), + bcc_addr=email_data.get('bcc_addr'), + date=email_data.get('date'), + message_id=email_data.get('message_id'), + body_text=email_data.get('body_text'), + body_html=email_data.get('body_html'), + is_read=email_data.get('is_read', False), + is_important=email_data.get('is_important', False), + folder=email_data.get('folder'), + attachments=attachments + ) + + # Only create raw_message if there are attachments (for attachment display support) + if email_data.get('attachments'): + # Create a minimal email.message.Message object for JSON imports to support attachment display + import email.message + from email.mime.multipart import MIMEMultipart + from email.mime.base import MIMEBase + from email.mime.text import MIMEText + + msg = MIMEMultipart() + msg['Subject'] = email_data['subject'] + msg['From'] = email_data['from_addr'] + msg['To'] = email_data['to_addr'] + if email_data.get('cc_addr'): + msg['Cc'] = email_data['cc_addr'] + if email_data.get('message_id'): + msg['Message-ID'] = email_data['message_id'] + if email_data.get('date'): + msg['Date'] = email_data['date'] + + # Add text body if exists + if email_data.get('body_text'): + text_part = MIMEText(email_data['body_text'], 'plain', 'utf-8') + msg.attach(text_part) + + # Add HTML body if exists + if email_data.get('body_html'): + html_part = MIMEText(email_data['body_html'], 'html', 'utf-8') + msg.attach(html_part) + + # Add attachments to the message object + for att_data in email_data.get('attachments', []): + if att_data.get('content'): + content_type_parts = att_data['content_type'].split('/') + if len(content_type_parts) == 2: + maintype, subtype = content_type_parts + else: + maintype, subtype = 'application', 'octet-stream' + + part = MIMEBase(maintype, subtype) + + # 方法A:直接使用(推荐) + # 验证 base64 格式但不解码 + try: + # 只验证,不实际解码 + base64.b64decode(att_data['content']) + # 如果验证通过,直接使用 + part.set_payload(att_data['content']) + part['Content-Transfer-Encoding'] = 'base64' + except Exception as e: + logging.warning(f"Invalid base64 content: {e}") + # 设置空附件 + part.set_payload('') + part['Content-Transfer-Encoding'] = 'base64' + + # 处理文件名... + filename = att_data["filename"] + try: + filename.encode('ascii') + part.add_header('Content-Disposition', 'attachment', + filename=filename) + except UnicodeEncodeError: + part.add_header('Content-Disposition', 'attachment', + filename=('utf-8', '', filename)) + + msg.attach(part) + + email_obj.raw_message = msg + else: + # No attachments, no need for raw_message + email_obj.raw_message = None + + emails.append(email_obj) + + except KeyError as e: + raise ValidationError(f"Missing required field in JSON: {str(e)}") + + # Sort emails by email_id in descending order + emails.sort(key=lambda email_obj: int(email_obj.email_id) if email_obj.email_id.isdigit() else 0, reverse=True) + + # import pickle + # os.makedirs("./json", exist_ok=True) + # with open("./json/all.pkl", "wb") as f: + # pickle.dump(emails, f) + + return emails + + def _import_from_eml(self, import_file: Path) -> List[EmailMessage]: + """Import single email from EML format""" + with open(import_file, 'rb') as f: + raw_email = f.read() + + email_id = import_file.stem + email_obj = parse_raw_email(raw_email, email_id) + # import pickle + # os.makedirs("./eml", exist_ok=True) + # with open("./eml/all.pkl", "wb") as f: + # pickle.dump([email_obj], f) + return [email_obj] + + def _import_from_directory(self, import_dir: Path) -> List[EmailMessage]: + """Import multiple emails from directory of EML files""" + emails = [] + + for eml_file in import_dir.glob('*.eml'): + try: + imported_emails = self._import_from_eml(eml_file) + emails.extend(imported_emails) + except Exception as e: + # Log warning but continue with other files + import logging + logging.warning(f"Failed to import {eml_file}: {str(e)}") + + return emails + + def save_attachment(self, attachment_data: bytes, filename: str) -> str: + """Save attachment data to file using configured download path""" + + # Use configured download path or current directory + if self.attachment_download_path: + download_dir = self.attachment_download_path + else: + download_dir = Path.cwd() + + try: + download_dir.mkdir(parents=True, exist_ok=True) + + file_path = download_dir / filename + + # Avoid overwriting existing files using (1), (2), etc. format + if file_path.exists(): + name, ext = os.path.splitext(filename) + counter = 1 + while True: + new_filename = f"{name}({counter}){ext}" + file_path = download_dir / new_filename + if not file_path.exists(): + break + counter += 1 + + with open(file_path, 'wb') as f: + f.write(attachment_data) + + return str(file_path) + + except Exception as e: + raise ValidationError(f"Failed to save attachment: {str(e)}") \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/imap_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/imap_backend.py new file mode 100644 index 00000000..5a6a61ec --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/imap_backend.py @@ -0,0 +1,546 @@ +import imaplib +import logging +from typing import List, Optional, Tuple +from datetime import datetime +from ..models.config import EmailConfig +from ..models.email import EmailFolder, EmailMessage +from ..utils.exceptions import ConnectionError, AuthenticationError, FolderError +from ..utils.email_parser import parse_raw_email +from ..utils.encode_decode import encode_to_imap_utf7, decode_from_imap_utf7 + +class IMAPBackend: + """IMAP backend for email operations""" + + def __init__(self, config: EmailConfig): + self.config = config + self.connection: Optional[imaplib.IMAP4_SSL] = None + self.current_folder: Optional[str] = None + self.last_accessed = datetime.now() + self.utf8_enabled = False + + def connect(self) -> bool: + """Establish IMAP connection""" + try: + if self.config.use_ssl: + self.connection = imaplib.IMAP4_SSL( + self.config.imap_server, + self.config.imap_port + ) + else: + self.connection = imaplib.IMAP4( + self.config.imap_server, + self.config.imap_port + ) + + # Login + self.connection.login(self.config.email, self.config.password) + self.last_accessed = datetime.now() + + # Try to enable UTF-8 support if available + self._enable_utf8_support() + + logging.info(f"IMAP connected for {self.config.email}") + return True + + except imaplib.IMAP4.error as e: + logging.error(f"IMAP authentication failed: {str(e)}") + raise AuthenticationError(f"IMAP login failed: {str(e)}") + except Exception as e: + logging.error(f"IMAP connection failed: {str(e)}") + raise ConnectionError(f"IMAP connection failed: {str(e)}") + + def disconnect(self): + """Close IMAP connection""" + if self.connection: + try: + self.connection.close() + self.connection.logout() + except: + pass + finally: + self.connection = None + self.current_folder = None + self.utf8_enabled = False + + def ensure_connected(self): + """Ensure IMAP connection is active""" + if not self.connection: + self.connect() + + # Test connection with NOOP + try: + self.connection.noop() + self.last_accessed = datetime.now() + except: + logging.warning("IMAP connection lost, reconnecting...") + self.disconnect() + self.connect() + + def _enable_utf8_support(self): + """Try to enable UTF-8 support on IMAP server""" + try: + # Check if server supports UTF8 capability + capabilities = self.connection.capability() + if capabilities[0] == 'OK': + capability_list = capabilities[1][0].decode().upper().split() + if 'UTF8=ACCEPT' in capability_list or 'UTF8=ONLY' in capability_list: + # Try to enable UTF-8 support + result = self.connection.enable('UTF8=ACCEPT') + if result[0] == 'OK': + self.utf8_enabled = True + logging.info("UTF-8 support enabled for IMAP connection") + else: + logging.warning("Failed to enable UTF-8 support") + else: + logging.info("Server does not support UTF8=ACCEPT capability") + except Exception as e: + logging.warning(f"Could not check/enable UTF-8 support: {str(e)}") + self.utf8_enabled = False + + def _quote_folder_name(self, folder_name: str) -> str: + """Quote folder name if it contains spaces (excluding leading/trailing spaces)""" + # Strip leading/trailing spaces first + folder_name = folder_name.strip() + + # Check if there are spaces in the middle of the folder name + if ' ' in folder_name: + # Escape any existing quotes in the folder name + folder_name = folder_name.replace('"', '\\"') + # Wrap with quotes + return f'"{folder_name}"' + + return folder_name + + def select_folder(self, folder: str) -> Tuple[int, int]: + """Select email folder and return (total_messages, unread_messages)""" + self.ensure_connected() + logging.info(f"尝试选中文件夹: {folder}") + try: + # Quote folder name if necessary + quoted_folder_name = self._quote_folder_name(folder) + utf7_quoted_folder_name = encode_to_imap_utf7(quoted_folder_name) + + if self.utf8_enabled: + status, data = self.connection.select(quoted_folder_name) + else: + status, data = self.connection.select(utf7_quoted_folder_name) + + if status != 'OK': + raise FolderError(f"Failed to select folder '{folder}': {status}") + + self.current_folder = folder # Store the original folder name without quotes + total_messages = int( + data[0]) if data[0] else 0 + + # Get unread count + status, unread_data = self.connection.search(None, 'UNSEEN') + unread_messages = len(unread_data[0].split()) if status == 'OK' and unread_data[0] else 0 + + return total_messages, unread_messages + + except Exception as e: + raise FolderError(f"Error selecting folder '{folder}': {str(e)}") + + def list_folders(self) -> List[EmailFolder]: + """List all available folders""" + self.ensure_connected() + + try: + status, folders = self.connection.list() + if status != 'OK': + raise FolderError(f"Failed to list folders: {status}") + + folder_list = [] + for folder in folders: + if self.utf8_enabled: + folder_info = folder.decode('utf-8') + else: + folder_info = decode_from_imap_utf7(folder.decode('utf-8')) + logging.debug(f"Parsing folder info: {folder_info}") + + # Parse folder name from IMAP response + # Format can be: '(\\HasNoChildren) "." "INBOX"' or '(\\HasNoChildren) "." INBOX' + parts = folder_info.split('"') + + folder_name = None + if len(parts) >= 3: + # If folder name is quoted: '(flags) "sep" "name"' + # The folder name is typically the last quoted part (parts[3] if exists, otherwise parts[2]) + if len(parts) >= 4 and parts[3].strip(): + folder_name = parts[3].strip() + elif len(parts) >= 3 and parts[2].strip() and parts[2].strip() not in ['.', '/', '\\']: + folder_name = parts[2].strip() + else: + # If folder name is not quoted: '(flags) "sep" name' + # Split by spaces and take the last part + space_parts = folder_info.split() + if len(space_parts) >= 3: + folder_name = space_parts[-1] + + # Skip invalid folder names (root folders, empty names, etc.) + if not folder_name or folder_name in [".", ".."]: + continue + + # Check if folder is selectable + is_noselect = '\\Noselect' in folder_info + + # Try to get folder stats for selectable folders + if not is_noselect: + try: + total, unread = self.select_folder(folder_name) + folder_obj = EmailFolder( + name=folder_name, + total_messages=total, + unread_messages=unread, + can_select=True + ) + except: + # If we can't select the folder, mark it as non-selectable + folder_obj = EmailFolder( + name=folder_name, + can_select=False + ) + else: + # Folder marked as non-selectable by server + folder_obj = EmailFolder( + name=folder_name, + can_select=False + ) + + folder_list.append(folder_obj) + + return folder_list + + except Exception as e: + raise FolderError(f"Error listing folders: {str(e)}") + + def get_email_ids(self, folder: str, limit: Optional[int] = None) -> List[str]: + """Get email IDs from folder (newest first)""" + quoted_folder_name = self._quote_folder_name(folder) + utf7_quoted_folder_name = encode_to_imap_utf7(quoted_folder_name) + + if self.utf8_enabled: + total, _ = self.select_folder(quoted_folder_name) + else: + total, _ = self.select_folder(utf7_quoted_folder_name) + + if total == 0: + return [] + + try: + # Get all email IDs + status, email_ids = self.connection.search(None, 'ALL') + if status != 'OK': + raise FolderError(f"Failed to search emails: {status}") + + id_list = email_ids[0].split() + # Reverse to get newest first + id_list = [uid.decode() for uid in reversed(id_list)] + + if limit: + id_list = id_list[:limit] + + return id_list + + except Exception as e: + raise FolderError(f"Error getting email IDs: {str(e)}") + + def fetch_email(self, email_id: str) -> EmailMessage: + """Fetch single email by ID""" + self.ensure_connected() + + try: + # Fetch email content and flags separately for better reliability + # First get the RFC822 content + status, content_data = self.connection.fetch(email_id, '(RFC822)') + if status != 'OK': + raise FolderError(f"Failed to fetch email content {email_id}: {status}") + + # Then get current flags separately + status, flag_data = self.connection.fetch(email_id, '(FLAGS)') + if status != 'OK': + raise FolderError(f"Failed to fetch email flags {email_id}: {status}") + + # Extract email content + raw_email = None + if content_data and len(content_data) > 0: + for item in content_data: + if isinstance(item, tuple) and len(item) == 2: + # item[1] should be the email content + if isinstance(item[1], bytes) and len(item[1]) > 0: + raw_email = item[1] + break + + if not raw_email: + raise FolderError(f"No email content found for email {email_id}") + + # Extract flags + flags = [] + for item in flag_data: + if isinstance(item, bytes): + # Direct bytes response like b'6 (FLAGS (\\Seen \\Flagged))' + header = item.decode() + if 'FLAGS' in header: + import re + flag_match = re.search(r'FLAGS \(([^)]*)\)', header) + if flag_match: + flags_str = flag_match.group(1) + flags = flags_str.split() if flags_str.strip() else [] + logging.debug(f"Extracted current flags for email {email_id}: {flags}") + break + elif isinstance(item, tuple) and len(item) == 2: + # Tuple response + header = item[0].decode() if isinstance(item[0], bytes) else str(item[0]) + if 'FLAGS' in header: + import re + flag_match = re.search(r'FLAGS \(([^)]*)\)', header) + if flag_match: + flags_str = flag_match.group(1) + flags = flags_str.split() if flags_str.strip() else [] + logging.debug(f"Extracted current flags for email {email_id}: {flags}") + break + + email_obj = parse_raw_email(raw_email, email_id) + email_obj.folder = self.current_folder + + # Set status based on current IMAP flags + email_obj.is_read = '\\Seen' in flags + email_obj.is_important = '\\Flagged' in flags + + logging.debug(f"Email {email_id} status: read={email_obj.is_read}, important={email_obj.is_important}") + + return email_obj + + except Exception as e: + logging.error(f"Error fetching email {email_id}: {str(e)}") + raise + + def search_emails(self, query: str, folder: str = None) -> List[str]: + """Search emails and return email IDs""" + self.ensure_connected() + + # If no folder specified, use INBOX as default + if not folder: + folder = 'INBOX' + + # Always select folder before searching + self.select_folder(folder) + + try: + # Handle Unicode/Chinese characters in search query + # Based on StackOverflow solution: need to encode Unicode strings to UTF-8 bytes + query_bytes = query.encode('utf-8') + + # Try UTF-8 search first if server supports it + # Use TEXT search which covers all text content (subject, body, headers) + # This is more reliable than OR combinations for UTF-8 content + if self.utf8_enabled: + # When UTF-8 is enabled, we should NOT specify charset parameter + # Use TEXT to search all text content + search_criteria = b'TEXT "' + query_bytes + b'"' + status, email_ids = self.connection.search(None, search_criteria) + else: + # For servers without UTF-8 support, try UTF-8 charset parameter + try: + # Use TEXT with UTF-8 charset + search_criteria = b'TEXT "' + query_bytes + b'"' + status, email_ids = self.connection.search('UTF-8', search_criteria) + except Exception as search_error: + logging.warning(f"UTF-8 charset search failed: {search_error}") + status = 'NO' + + if status != 'OK': + # Fallback to ASCII search if UTF-8 search fails + logging.warning(f"UTF-8 search failed, trying ASCII fallback for query: {query}") + ascii_query = query.encode('ascii', errors='ignore').decode('ascii') + if ascii_query.strip(): # Only search if we have non-empty ASCII query + search_criteria_ascii = f'(OR SUBJECT "{ascii_query}" FROM "{ascii_query}" BODY "{ascii_query}")' + status, email_ids = self.connection.search(None, search_criteria_ascii) + else: + # If ASCII conversion results in empty string, return empty results + logging.warning(f"Query '{query}' contains only non-ASCII characters, no ASCII fallback possible") + return [] + + if status != 'OK': + raise FolderError(f"Search failed: {status}") + + id_list = email_ids[0].split() + # Return newest first + return [uid.decode() for uid in reversed(id_list)] + + except Exception as e: + logging.error(f"Error searching emails with query '{query}': {str(e)}") + raise FolderError(f"Error searching emails: {str(e)}") + + def mark_as_read(self, email_id: str) -> bool: + """Mark email as read + + Returns: + bool: True if operation was successful + """ + self.ensure_connected() + + try: + result = self.connection.store(email_id, '+FLAGS', '\\Seen') + if result[0] != 'OK': + logging.error(f"Failed to mark email {email_id} as read: {result[1]}") + return False + + # Force synchronization to ensure the change is applied + self.connection.noop() # Send NOOP to sync with server + logging.debug(f"Successfully marked email {email_id} as read") + return True + + except Exception as e: + logging.error(f"Error marking email {email_id} as read: {str(e)}") + return False + + def mark_as_unread(self, email_id: str) -> bool: + """Mark email as unread + + Returns: + bool: True if operation was successful + """ + self.ensure_connected() + + try: + result = self.connection.store(email_id, '-FLAGS', '\\Seen') + if result[0] != 'OK': + logging.error(f"Failed to mark email {email_id} as unread: {result[1]}") + return False + + # Force synchronization to ensure the change is applied + self.connection.noop() # Send NOOP to sync with server + logging.debug(f"Successfully marked email {email_id} as unread") + return True + + except Exception as e: + logging.error(f"Error marking email {email_id} as unread: {str(e)}") + return False + + def mark_as_important(self, email_id: str) -> bool: + """Mark email as important (flagged) + + Returns: + bool: True if operation was successful + """ + self.ensure_connected() + + try: + result = self.connection.store(email_id, '+FLAGS', '\\Flagged') + if result[0] != 'OK': + logging.error(f"Failed to mark email {email_id} as important: {result[1]}") + return False + + # Force synchronization to ensure the change is applied + self.connection.noop() # Send NOOP to sync with server + logging.debug(f"Successfully marked email {email_id} as important") + return True + + except Exception as e: + logging.error(f"Error marking email {email_id} as important: {str(e)}") + return False + + def mark_as_not_important(self, email_id: str) -> bool: + """Remove important flag from email + + Returns: + bool: True if operation was successful + """ + self.ensure_connected() + + try: + result = self.connection.store(email_id, '-FLAGS', '\\Flagged') + if result[0] != 'OK': + logging.error(f"Failed to remove important flag from email {email_id}: {result[1]}") + return False + + # Force synchronization to ensure the change is applied + self.connection.noop() # Send NOOP to sync with server + logging.debug(f"Successfully removed important flag from email {email_id}") + return True + + except Exception as e: + logging.error(f"Error removing important flag from email {email_id}: {str(e)}") + return False + + def delete_email(self, email_id: str): + """Mark email for deletion""" + self.ensure_connected() + + try: + self.connection.store(email_id, '+FLAGS', '\\Deleted') + self.connection.expunge() + except Exception as e: + logging.error(f"Error deleting email {email_id}: {str(e)}") + raise FolderError(f"Failed to delete email: {str(e)}") + + def move_email(self, email_id: str, target_folder: str) -> Optional[str]: + """Move email to another folder with UTF-8 encoding support + + Returns: + Optional[str]: New email ID in target folder, or None if move failed + """ + self.ensure_connected() + + try: + # First, verify the email exists in current folder + status, data = self.connection.fetch(email_id, '(FLAGS)') + if status != 'OK': + raise FolderError(f"Email {email_id} not found in current folder") + + # Handle UTF-8 encoding for target folder name + quoted_target_folder = self._quote_folder_name(target_folder) + utf7_quoted_target_folder = encode_to_imap_utf7(quoted_target_folder) + + # Copy to target folder + if self.utf8_enabled: + copy_result = self.connection.copy(email_id, quoted_target_folder) + else: + copy_result = self.connection.copy(email_id, utf7_quoted_target_folder) + if copy_result[0] != 'OK': + raise FolderError(f"Failed to copy email to {target_folder}: {copy_result[1]}") + + # Mark as deleted in current folder + store_result = self.connection.store(email_id, '+FLAGS', '\\Deleted') + if store_result[0] != 'OK': + logging.warning(f"Failed to mark email {email_id} as deleted: {store_result[1]}") + + # Expunge to actually remove from current folder + expunge_result = self.connection.expunge() + if expunge_result[0] != 'OK': + logging.warning(f"Failed to expunge deleted emails: {expunge_result[1]}") + + logging.info(f"Successfully moved email {email_id} to {target_folder}") + + # Note: We can't easily get the new UID without searching, + # but the move operation itself is successful + return None # Could be enhanced to return new UID if needed + + except Exception as e: + logging.error(f"Error moving email {email_id} to {target_folder}: {str(e)}") + raise FolderError(f"Failed to move email: {str(e)}") + + def append_message(self, folder: str, message: str, flags: str = '\\Seen') -> bool: + """Append a message to the specified folder with UTF-8 support""" + self.ensure_connected() + + try: + # Encode folder name for IMAP operations + quoted_folder = self._quote_folder_name(folder) + utf7_quoted_folder = encode_to_imap_utf7(quoted_folder) + + # Use IMAP APPEND command to add message to folder + if self.utf8_enabled: + result = self.connection.append(quoted_folder, flags, None, message.encode('utf-8')) + else: + result = self.connection.append(utf7_quoted_folder, flags, None, message.encode('utf-8')) + if result[0] == 'OK': + logging.info(f"Message appended to {folder}") + return True + else: + logging.error(f"Failed to append message to {folder}: {result}") + return False + except Exception as e: + logging.error(f"Error appending message to {folder}: {str(e)}") + raise FolderError(f"Failed to append message to {folder}: {str(e)}") \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/pg_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/pg_backend.py new file mode 100644 index 00000000..073bc9a7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/pg_backend.py @@ -0,0 +1,818 @@ +import json +import logging +import os +import uuid +from datetime import datetime +from email import policy +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email import encoders +from email.parser import Parser +from email.utils import formataddr, formatdate, make_msgid +from typing import List, Optional, Tuple + +import psycopg2 +import psycopg2.extras + +from ..models.config import EmailConfig +from ..models.email import EmailAttachment, EmailFolder, EmailMessage +from ..utils.exceptions import ConnectionError, FolderError, SendEmailError + + +def _get_pg_conn_params() -> dict: + """Get PostgreSQL connection parameters from environment variables.""" + return { + "host": os.environ.get("PG_HOST", "localhost"), + "port": int(os.environ.get("PG_PORT", "5432")), + "database": os.environ.get("PG_DATABASE", "toolathlon"), + "user": os.environ.get("PG_USER", "postgres"), + "password": os.environ.get("PG_PASSWORD", "postgres"), + } + + +def _jsonb_list_to_comma_str(jsonb_val) -> str: + """Convert a JSONB list (or Python list) to a comma-separated string.""" + if jsonb_val is None: + return "" + if isinstance(jsonb_val, str): + try: + jsonb_val = json.loads(jsonb_val) + except (json.JSONDecodeError, TypeError): + return jsonb_val + if isinstance(jsonb_val, list): + return ", ".join(str(v) for v in jsonb_val if v) + return str(jsonb_val) + + +def _comma_str_to_jsonb_list(comma_str: Optional[str]) -> list: + """Convert a comma-separated string to a JSON-serializable list.""" + if not comma_str: + return [] + return [addr.strip() for addr in comma_str.split(",") if addr.strip()] + + +class PgIMAPBackend: + """PostgreSQL-backed IMAP replacement backend for email operations.""" + + def __init__(self, config: EmailConfig): + self.config = config + self.connection = None # psycopg2 connection + self.current_folder: Optional[str] = None + self.utf8_enabled = True # Always true for PG + + # ------------------------------------------------------------------ + # Connection management + # ------------------------------------------------------------------ + + def connect(self) -> bool: + """Establish PostgreSQL connection.""" + try: + self.connection = psycopg2.connect(**_get_pg_conn_params()) + self.connection.autocommit = True + logging.info("PgIMAPBackend connected to PostgreSQL") + return True + except Exception as e: + logging.error(f"PgIMAPBackend connection failed: {e}") + raise ConnectionError(f"PostgreSQL connection failed: {e}") + + def disconnect(self): + """Close PostgreSQL connection.""" + if self.connection: + try: + self.connection.close() + except Exception: + pass + finally: + self.connection = None + self.current_folder = None + + def ensure_connected(self): + """Ensure PostgreSQL connection is active.""" + if self.connection is None or self.connection.closed: + self.connect() + return + try: + with self.connection.cursor() as cur: + cur.execute("SELECT 1") + except Exception: + logging.warning("PgIMAPBackend connection lost, reconnecting...") + self.disconnect() + self.connect() + + # ------------------------------------------------------------------ + # Folder operations + # ------------------------------------------------------------------ + + def _get_or_create_folder(self, folder_name: str) -> int: + """Return folder id, creating the folder row if it doesn't exist.""" + self.ensure_connected() + with self.connection.cursor() as cur: + cur.execute( + "INSERT INTO email.folders (name) VALUES (%s) " + "ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name " + "RETURNING id", + (folder_name,), + ) + return cur.fetchone()[0] + + def _refresh_folder_counts(self, folder_id: int): + """Refresh message_count and unread_count for a folder.""" + with self.connection.cursor() as cur: + cur.execute( + "UPDATE email.folders SET " + "message_count = (SELECT COUNT(*) FROM email.messages WHERE folder_id = %s), " + "unread_count = (SELECT COUNT(*) FROM email.messages WHERE folder_id = %s AND is_read = FALSE) " + "WHERE id = %s", + (folder_id, folder_id, folder_id), + ) + + def select_folder(self, folder: str) -> Tuple[int, int]: + """Select email folder and return (total_messages, unread_messages).""" + self.ensure_connected() + folder = folder.strip() + folder_id = self._get_or_create_folder(folder) + self._refresh_folder_counts(folder_id) + self.current_folder = folder + + with self.connection.cursor() as cur: + cur.execute( + "SELECT message_count, unread_count FROM email.folders WHERE id = %s", + (folder_id,), + ) + row = cur.fetchone() + if row is None: + raise FolderError(f"Folder '{folder}' not found") + return row[0], row[1] + + def list_folders(self) -> List[EmailFolder]: + """List all available folders.""" + self.ensure_connected() + with self.connection.cursor() as cur: + cur.execute("SELECT id, name, message_count, unread_count FROM email.folders ORDER BY name") + rows = cur.fetchall() + + folders = [] + for row in rows: + folder_id, name, msg_count, unread_count = row + # Refresh counts for accuracy + self._refresh_folder_counts(folder_id) + with self.connection.cursor() as cur: + cur.execute( + "SELECT message_count, unread_count FROM email.folders WHERE id = %s", + (folder_id,), + ) + updated = cur.fetchone() + folders.append( + EmailFolder( + name=name, + total_messages=updated[0] if updated else msg_count, + unread_messages=updated[1] if updated else unread_count, + can_select=True, + ) + ) + return folders + + def create_folder(self, folder_name: str) -> bool: + """Create a new email folder.""" + self.ensure_connected() + folder_name = folder_name.strip() + try: + self._get_or_create_folder(folder_name) + logging.info(f"Folder '{folder_name}' created (or already exists)") + return True + except Exception as e: + logging.error(f"Error creating folder '{folder_name}': {e}") + raise FolderError(f"Failed to create folder: {e}") + + def delete_folder(self, folder_name: str) -> bool: + """Delete an email folder and all its messages.""" + self.ensure_connected() + folder_name = folder_name.strip() + try: + with self.connection.cursor() as cur: + cur.execute("SELECT id FROM email.folders WHERE name = %s", (folder_name,)) + row = cur.fetchone() + if row is None: + raise FolderError(f"Folder '{folder_name}' not found") + folder_id = row[0] + # Delete messages in folder (attachments cascade) + cur.execute("DELETE FROM email.messages WHERE folder_id = %s", (folder_id,)) + cur.execute("DELETE FROM email.folders WHERE id = %s", (folder_id,)) + logging.info(f"Folder '{folder_name}' deleted") + return True + except FolderError: + raise + except Exception as e: + logging.error(f"Error deleting folder '{folder_name}': {e}") + raise FolderError(f"Failed to delete folder: {e}") + + # ------------------------------------------------------------------ + # Email ID operations + # ------------------------------------------------------------------ + + def get_email_ids(self, folder: str, limit: Optional[int] = None) -> List[str]: + """Get email IDs from folder (newest first).""" + self.ensure_connected() + folder = folder.strip() + folder_id = self._get_or_create_folder(folder) + + query = ( + "SELECT m.id FROM email.messages m WHERE m.folder_id = %s ORDER BY m.date DESC, m.id DESC" + ) + params: list = [folder_id] + if limit is not None: + query += " LIMIT %s" + params.append(limit) + + with self.connection.cursor() as cur: + cur.execute(query, params) + rows = cur.fetchall() + return [str(r[0]) for r in rows] + + # ------------------------------------------------------------------ + # Fetch + # ------------------------------------------------------------------ + + def fetch_email(self, email_id: str) -> EmailMessage: + """Fetch single email by ID (primary key).""" + self.ensure_connected() + msg_pk = int(email_id) + + with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute( + "SELECT m.*, f.name AS folder_name " + "FROM email.messages m " + "JOIN email.folders f ON f.id = m.folder_id " + "WHERE m.id = %s", + (msg_pk,), + ) + row = cur.fetchone() + if row is None: + raise FolderError(f"Email {email_id} not found") + + # Fetch attachments + cur.execute( + "SELECT id, filename, content_type, size, content FROM email.attachments WHERE message_id = %s", + (msg_pk,), + ) + att_rows = cur.fetchall() + + attachments = [] + for a in att_rows: + attachments.append( + EmailAttachment( + filename=a["filename"], + content_type=a["content_type"] or "application/octet-stream", + size=a["size"] or 0, + attachment_id=str(a["id"]), + content=bytes(a["content"]) if a["content"] else None, + ) + ) + + date_val = row["date"] + date_str = date_val.isoformat() if isinstance(date_val, datetime) else str(date_val) if date_val else None + + email_obj = EmailMessage( + email_id=str(row["id"]), + subject=row["subject"] or "", + from_addr=row["from_addr"] or "", + to_addr=_jsonb_list_to_comma_str(row["to_addr"]), + cc_addr=_jsonb_list_to_comma_str(row["cc_addr"]) or None, + bcc_addr=_jsonb_list_to_comma_str(row["bcc_addr"]) or None, + date=date_str, + message_id=row["message_id"], + body_text=row["body_text"], + body_html=row["body_html"], + attachments=attachments, + is_read=row["is_read"], + is_important=row["is_important"], + folder=row["folder_name"], + ) + return email_obj + + # ------------------------------------------------------------------ + # Search + # ------------------------------------------------------------------ + + def search_emails(self, query: str, folder: str = None) -> List[str]: + """Search emails using PostgreSQL full-text search on subject and body_text.""" + self.ensure_connected() + + # Build tsquery from the raw query string. + # Split into words and join with '&' for AND semantics. + words = query.strip().split() + if not words: + return [] + ts_query_str = " & ".join(words) + + sql = ( + "SELECT m.id FROM email.messages m " + ) + params: list = [] + + if folder: + folder = folder.strip() + folder_id = self._get_or_create_folder(folder) + sql += "WHERE m.folder_id = %s AND " + params.append(folder_id) + else: + sql += "WHERE " + + sql += ( + "to_tsvector('simple', COALESCE(m.subject, '') || ' ' || COALESCE(m.body_text, '')) " + "@@ to_tsquery('simple', %s) " + "ORDER BY m.date DESC, m.id DESC" + ) + params.append(ts_query_str) + + with self.connection.cursor() as cur: + cur.execute(sql, params) + rows = cur.fetchall() + return [str(r[0]) for r in rows] + + # ------------------------------------------------------------------ + # Flag operations + # ------------------------------------------------------------------ + + def mark_as_read(self, email_id: str) -> bool: + self.ensure_connected() + try: + with self.connection.cursor() as cur: + cur.execute( + "UPDATE email.messages SET is_read = TRUE WHERE id = %s", (int(email_id),) + ) + return True + except Exception as e: + logging.error(f"Error marking email {email_id} as read: {e}") + return False + + def mark_as_unread(self, email_id: str) -> bool: + self.ensure_connected() + try: + with self.connection.cursor() as cur: + cur.execute( + "UPDATE email.messages SET is_read = FALSE WHERE id = %s", (int(email_id),) + ) + return True + except Exception as e: + logging.error(f"Error marking email {email_id} as unread: {e}") + return False + + def mark_as_important(self, email_id: str) -> bool: + self.ensure_connected() + try: + with self.connection.cursor() as cur: + cur.execute( + "UPDATE email.messages SET is_important = TRUE WHERE id = %s", (int(email_id),) + ) + return True + except Exception as e: + logging.error(f"Error marking email {email_id} as important: {e}") + return False + + def mark_as_not_important(self, email_id: str) -> bool: + self.ensure_connected() + try: + with self.connection.cursor() as cur: + cur.execute( + "UPDATE email.messages SET is_important = FALSE WHERE id = %s", (int(email_id),) + ) + return True + except Exception as e: + logging.error(f"Error removing important flag from email {email_id}: {e}") + return False + + # ------------------------------------------------------------------ + # Delete / Move / Append + # ------------------------------------------------------------------ + + def delete_email(self, email_id: str): + """Delete email from database.""" + self.ensure_connected() + try: + with self.connection.cursor() as cur: + cur.execute("DELETE FROM email.messages WHERE id = %s", (int(email_id),)) + except Exception as e: + logging.error(f"Error deleting email {email_id}: {e}") + raise FolderError(f"Failed to delete email: {e}") + + def move_email(self, email_id: str, target_folder: str) -> Optional[str]: + """Move email to another folder. Returns None (consistent with IMAP backend).""" + self.ensure_connected() + target_folder = target_folder.strip() + target_folder_id = self._get_or_create_folder(target_folder) + + try: + with self.connection.cursor() as cur: + cur.execute( + "UPDATE email.messages SET folder_id = %s WHERE id = %s", + (target_folder_id, int(email_id)), + ) + logging.info(f"Moved email {email_id} to folder '{target_folder}'") + return None + except Exception as e: + logging.error(f"Error moving email {email_id} to {target_folder}: {e}") + raise FolderError(f"Failed to move email: {e}") + + def append_message(self, folder: str, message: str, flags: str = "\\Seen") -> bool: + """Parse an RFC822 message string and insert it into the specified folder.""" + self.ensure_connected() + folder = folder.strip() + folder_id = self._get_or_create_folder(folder) + + try: + parser = Parser(policy=policy.default) + msg = parser.parsestr(message) + + subject = msg.get("Subject", "") + from_addr = msg.get("From", "") + to_addr = msg.get("To", "") + cc_addr = msg.get("Cc", "") + bcc_addr = msg.get("Bcc", "") + message_id_hdr = msg.get("Message-ID", None) + in_reply_to = msg.get("In-Reply-To", None) + references_hdr = msg.get("References", None) + date_hdr = msg.get("Date", None) + + # Extract body + body_text = None + body_html = None + if msg.is_multipart(): + for part in msg.walk(): + ct = part.get_content_type() + if ct == "text/plain" and body_text is None: + body_text = part.get_content() + elif ct == "text/html" and body_html is None: + body_html = part.get_content() + else: + ct = msg.get_content_type() + content = msg.get_content() + if ct == "text/html": + body_html = content + else: + body_text = content + + is_read = "\\Seen" in flags + is_flagged = "\\Flagged" in flags + + to_list = _comma_str_to_jsonb_list(to_addr) + cc_list = _comma_str_to_jsonb_list(cc_addr) + bcc_list = _comma_str_to_jsonb_list(bcc_addr) + + with self.connection.cursor() as cur: + cur.execute( + "INSERT INTO email.messages " + "(folder_id, message_id, subject, from_addr, to_addr, cc_addr, bcc_addr, " + "date, body_text, body_html, is_read, is_flagged, in_reply_to, references_header, " + "size) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, " + "COALESCE(%s::timestamptz, NOW()), %s, %s, %s, %s, %s, %s, %s) " + "RETURNING id", + ( + folder_id, + message_id_hdr, + subject, + from_addr, + json.dumps(to_list), + json.dumps(cc_list), + json.dumps(bcc_list), + date_hdr, + body_text, + body_html, + is_read, + is_flagged, + in_reply_to, + references_hdr, + len(message), + ), + ) + new_id = cur.fetchone()[0] + + # Handle attachments from multipart message + if msg.is_multipart(): + for part in msg.iter_attachments(): + filename = part.get_filename() or "attachment" + content_type = part.get_content_type() + payload = part.get_payload(decode=True) + size = len(payload) if payload else 0 + content_id = part.get("Content-ID", None) + with self.connection.cursor() as cur: + cur.execute( + "INSERT INTO email.attachments " + "(message_id, filename, content_type, size, content, content_id) " + "VALUES (%s, %s, %s, %s, %s, %s)", + (new_id, filename, content_type, size, + psycopg2.Binary(payload) if payload else None, content_id), + ) + + logging.info(f"Message appended to folder '{folder}' with id {new_id}") + return True + + except Exception as e: + logging.error(f"Error appending message to folder '{folder}': {e}") + raise FolderError(f"Failed to append message to {folder}: {e}") + + +class PgSMTPBackend: + """PostgreSQL-backed SMTP replacement backend. + + Instead of actually sending emails over SMTP, this backend inserts + the message into the ``email.messages`` table (in the 'Sent' folder) + and logs the send in ``email.sent_log``. + """ + + def __init__(self, config: EmailConfig): + self.config = config + self.connection = None # psycopg2 connection + + # ------------------------------------------------------------------ + # Connection management + # ------------------------------------------------------------------ + + def connect(self) -> bool: + """Establish PostgreSQL connection.""" + try: + self.connection = psycopg2.connect(**_get_pg_conn_params()) + self.connection.autocommit = True + logging.info("PgSMTPBackend connected to PostgreSQL") + return True + except Exception as e: + logging.error(f"PgSMTPBackend connection failed: {e}") + raise ConnectionError(f"PostgreSQL connection failed: {e}") + + def disconnect(self): + """Close PostgreSQL connection.""" + if self.connection: + try: + self.connection.close() + except Exception: + pass + finally: + self.connection = None + + def ensure_connected(self): + """Ensure PostgreSQL connection is active.""" + if self.connection is None or self.connection.closed: + self.connect() + return + try: + with self.connection.cursor() as cur: + cur.execute("SELECT 1") + except Exception: + logging.warning("PgSMTPBackend connection lost, reconnecting...") + self.disconnect() + self.connect() + + # ------------------------------------------------------------------ + # Send (insert into DB) + # ------------------------------------------------------------------ + + def send_email( + self, + to: str, + subject: str, + body: str, + html_body: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None, + attachments: Optional[List[str]] = None, + ) -> tuple: + """Compose and 'send' an email by inserting into the database. + + Returns: + tuple[bool, Optional[str]]: (success, RFC822 message string) + """ + self.ensure_connected() + + try: + # ---- Build the MIME message (for the returned RFC822 string) ---- + if html_body or attachments: + msg = MIMEMultipart("alternative" if html_body and not attachments else "mixed") + text_part = MIMEText(body, "plain", "utf-8") + msg.attach(text_part) + if html_body: + html_part = MIMEText(html_body, "html", "utf-8") + msg.attach(html_part) + else: + msg = MIMEText(body, "plain", "utf-8") + + from email.header import Header + + msg["Subject"] = Header(subject, "utf-8") + if self.config.name: + encoded_name = Header(self.config.name, "utf-8") + msg["From"] = formataddr((str(encoded_name), self.config.email)) + else: + msg["From"] = self.config.email + msg["To"] = to + if cc: + msg["Cc"] = cc + if bcc: + msg["Bcc"] = bcc + msg["Date"] = formatdate(localtime=True) + msg["Message-ID"] = make_msgid() + + # Attachments + attachment_data_list = [] + if attachments: + if not isinstance(msg, MIMEMultipart): + original_msg = msg + msg = MIMEMultipart("mixed") + msg["Subject"] = Header(subject, "utf-8") + if self.config.name: + msg["From"] = formataddr((self.config.name, self.config.email)) + else: + msg["From"] = self.config.email + msg["To"] = to + if cc: + msg["Cc"] = cc + if bcc: + msg["Bcc"] = bcc + msg.attach(original_msg) + + for file_path in attachments: + filename = os.path.basename(file_path) + with open(file_path, "rb") as f: + file_data = f.read() + attachment_data_list.append((filename, "application/octet-stream", file_data)) + + att = MIMEBase("application", "octet-stream") + att.set_payload(file_data) + encoders.encode_base64(att) + att.add_header("Content-Disposition", f"attachment; filename= {filename}") + msg.attach(att) + + message_string = msg.as_string() + + # ---- Insert into database ---- + to_list = _comma_str_to_jsonb_list(to) + cc_list = _comma_str_to_jsonb_list(cc) + bcc_list = _comma_str_to_jsonb_list(bcc) + + # Ensure 'Sent' folder exists + with self.connection.cursor() as cur: + cur.execute( + "INSERT INTO email.folders (name) VALUES ('Sent') " + "ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name " + "RETURNING id" + ) + sent_folder_id = cur.fetchone()[0] + + cur.execute( + "INSERT INTO email.messages " + "(folder_id, message_id, subject, from_addr, to_addr, cc_addr, bcc_addr, " + "date, body_text, body_html, is_read, size) " + "VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), %s, %s, TRUE, %s) " + "RETURNING id", + ( + sent_folder_id, + msg["Message-ID"], + subject, + msg["From"], + json.dumps(to_list), + json.dumps(cc_list), + json.dumps(bcc_list), + body, + html_body, + len(message_string), + ), + ) + new_msg_id = cur.fetchone()[0] + + # Insert attachments + for filename, content_type, file_data in attachment_data_list: + cur.execute( + "INSERT INTO email.attachments " + "(message_id, filename, content_type, size, content) " + "VALUES (%s, %s, %s, %s, %s)", + (new_msg_id, filename, content_type, len(file_data), + psycopg2.Binary(file_data)), + ) + + # Log to sent_log + cur.execute( + "INSERT INTO email.sent_log (message_id) VALUES (%s)", + (new_msg_id,), + ) + + logging.info(f"Email 'sent' (stored) to {to} with id {new_msg_id}") + return True, message_string + + except Exception as e: + logging.error(f"Error sending email: {e}") + raise SendEmailError(f"Failed to send email: {e}") + + # ------------------------------------------------------------------ + # Test connection + # ------------------------------------------------------------------ + + def test_connection(self) -> bool: + """Test PostgreSQL connection.""" + try: + self.ensure_connected() + return True + except Exception: + return False + + +class PgDraftBackend: + """PostgreSQL-backed draft storage.""" + + def __init__(self): + self.connection = None + + def _ensure_connected(self): + if self.connection is None or self.connection.closed: + self.connection = psycopg2.connect(**_get_pg_conn_params()) + self.connection.autocommit = True + + def save_draft(self, subject, body, html_body=None, to=None, cc=None, bcc=None, from_addr=None): + self._ensure_connected() + to_list = _comma_str_to_jsonb_list(to) + cc_list = _comma_str_to_jsonb_list(cc) + bcc_list = _comma_str_to_jsonb_list(bcc) + with self.connection.cursor() as cur: + cur.execute( + "INSERT INTO email.drafts (subject, from_addr, to_addr, cc_addr, bcc_addr, body_text, body_html) " + "VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id", + (subject, from_addr, json.dumps(to_list), json.dumps(cc_list), json.dumps(bcc_list), body, html_body), + ) + return str(cur.fetchone()[0]) + + def get_drafts(self, page=1, page_size=20): + self._ensure_connected() + with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute("SELECT COUNT(*) FROM email.drafts") + total = cur.fetchone()[0] + offset = (page - 1) * page_size + cur.execute( + "SELECT * FROM email.drafts ORDER BY updated_at DESC LIMIT %s OFFSET %s", + (page_size, offset), + ) + rows = cur.fetchall() + total_pages = max(1, (total + page_size - 1) // page_size) + drafts = [] + for r in rows: + drafts.append({ + 'draft_id': str(r['id']), + 'subject': r['subject'] or '', + 'to': _jsonb_list_to_comma_str(r['to_addr']), + 'cc': _jsonb_list_to_comma_str(r['cc_addr']), + 'bcc': _jsonb_list_to_comma_str(r['bcc_addr']), + 'body': r['body_text'] or '', + 'html_body': r['body_html'], + 'created_at': r['created_at'].isoformat() if r['created_at'] else '', + 'updated_at': r['updated_at'].isoformat() if r['updated_at'] else '', + }) + return {'drafts': drafts, 'total_drafts': total, 'current_page': page, 'total_pages': total_pages, 'page_size': page_size} + + def get_draft(self, draft_id): + self._ensure_connected() + with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute("SELECT * FROM email.drafts WHERE id = %s", (int(draft_id),)) + r = cur.fetchone() + if r is None: + raise FolderError(f"Draft not found: {draft_id}") + return { + 'draft_id': str(r['id']), + 'subject': r['subject'] or '', + 'to': _jsonb_list_to_comma_str(r['to_addr']), + 'cc': _jsonb_list_to_comma_str(r['cc_addr']), + 'bcc': _jsonb_list_to_comma_str(r['bcc_addr']), + 'body': r['body_text'] or '', + 'html_body': r['body_html'], + 'created_at': r['created_at'].isoformat() if r['created_at'] else '', + 'updated_at': r['updated_at'].isoformat() if r['updated_at'] else '', + } + + def update_draft(self, draft_id, subject=None, body=None, html_body=None, to=None, cc=None, bcc=None): + self._ensure_connected() + sets = ["updated_at = NOW()"] + params = [] + if subject is not None: + sets.append("subject = %s"); params.append(subject) + if body is not None: + sets.append("body_text = %s"); params.append(body) + if html_body is not None: + sets.append("body_html = %s"); params.append(html_body) + if to is not None: + sets.append("to_addr = %s"); params.append(json.dumps(_comma_str_to_jsonb_list(to))) + if cc is not None: + sets.append("cc_addr = %s"); params.append(json.dumps(_comma_str_to_jsonb_list(cc))) + if bcc is not None: + sets.append("bcc_addr = %s"); params.append(json.dumps(_comma_str_to_jsonb_list(bcc))) + params.append(int(draft_id)) + with self.connection.cursor() as cur: + cur.execute(f"UPDATE email.drafts SET {', '.join(sets)} WHERE id = %s", params) + if cur.rowcount == 0: + raise FolderError(f"Draft not found: {draft_id}") + return True + + def delete_draft(self, draft_id): + self._ensure_connected() + with self.connection.cursor() as cur: + cur.execute("DELETE FROM email.drafts WHERE id = %s", (int(draft_id),)) + if cur.rowcount == 0: + raise FolderError(f"Draft not found: {draft_id}") + return True diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/smtp_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/smtp_backend.py new file mode 100644 index 00000000..cc4aee1b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/smtp_backend.py @@ -0,0 +1,233 @@ +import smtplib +import logging +import os +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email import encoders +from email.utils import formataddr +from typing import List, Optional +from ..models.config import EmailConfig +from ..utils.exceptions import ConnectionError, AuthenticationError, SendEmailError +from ..utils.validators import validate_email_list, validate_file_path +from ..config.settings import ConfigManager + + +class SMTPBackend: + """SMTP backend for sending emails""" + + def __init__(self, config: EmailConfig): + self.config = config + self.connection: Optional[smtplib.SMTP] = None + + def connect(self) -> bool: + """Establish SMTP connection""" + try: + self.connection = smtplib.SMTP( + self.config.smtp_server, + self.config.smtp_port + ) + + # Enable debugging for development + if logging.getLogger().isEnabledFor(logging.DEBUG): + self.connection.set_debuglevel(1) + + # Start TLS if configured + if self.config.use_starttls: + self.connection.starttls() + + # Login only if password is provided and server supports auth + if self.config.password and self.config.password.strip(): + try: + # Check if server supports authentication + if 'auth' in self.connection.esmtp_features: + self.connection.login(self.config.email, self.config.password) + logging.info(f"SMTP authenticated for {self.config.email}") + else: + logging.info(f"SMTP server doesn't support auth, proceeding without authentication") + except smtplib.SMTPAuthenticationError as e: + # If auth fails but server might allow sending without auth, try to continue + logging.warning(f"SMTP authentication failed: {str(e)}, trying without auth") + else: + logging.info(f"No password provided, proceeding without authentication") + + logging.info(f"SMTP connected for {self.config.email}") + return True + + except Exception as e: + logging.error(f"SMTP connection failed: {str(e)}") + raise ConnectionError(f"SMTP connection failed: {str(e)}") + + def disconnect(self): + """Close SMTP connection""" + if self.connection: + try: + self.connection.quit() + except: + pass + finally: + self.connection = None + + def ensure_connected(self): + """Ensure SMTP connection is active""" + if not self.connection: + logging.debug("No SMTP connection, connecting...") + self.connect() + + # Test connection with more robust checking + try: + status = self.connection.noop() + if status[0] != 250: # NOOP should return 250 OK + raise Exception(f"NOOP returned {status}") + except Exception as e: + logging.warning(f"SMTP connection test failed: {e}, reconnecting...") + self.disconnect() + self.connect() + + def send_email(self, to: str, subject: str, body: str, + html_body: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None, + attachments: Optional[List[str]] = None) -> tuple[bool, Optional[str]]: + """Send email with optional HTML, CC, BCC, and attachments + + Returns: + tuple[bool, Optional[str]]: (success, message_string_for_saving) + """ + + # Validate recipients + valid, error = validate_email_list(to) + if not valid: + raise SendEmailError(f"Invalid TO addresses: {error}") + + if cc: + valid, error = validate_email_list(cc) + if not valid: + raise SendEmailError(f"Invalid CC addresses: {error}") + + if bcc: + valid, error = validate_email_list(bcc) + if not valid: + raise SendEmailError(f"Invalid BCC addresses: {error}") + + self.ensure_connected() + + try: + # Create message + if html_body or attachments: + msg = MIMEMultipart('alternative' if html_body else 'mixed') + else: + msg = MIMEText(body, 'plain', 'utf-8') + + if isinstance(msg, MIMEMultipart): + # Add text part + text_part = MIMEText(body, 'plain', 'utf-8') + msg.attach(text_part) + + # Add HTML part if provided + if html_body: + html_part = MIMEText(html_body, 'html', 'utf-8') + msg.attach(html_part) + + # Set headers with proper Chinese encoding + from email.header import Header + + # Encode subject for Chinese characters + msg['Subject'] = Header(subject, 'utf-8') + + # Format From header with name if provided, with Chinese support + if self.config.name: + # Encode name for Chinese characters + encoded_name = Header(self.config.name, 'utf-8') + msg['From'] = formataddr((str(encoded_name), self.config.email)) + else: + msg['From'] = self.config.email + msg['To'] = to + + if cc: + msg['Cc'] = cc + if bcc: + msg['Bcc'] = bcc + + # Add attachments + if attachments: + # Ensure we have a multipart message + if not isinstance(msg, MIMEMultipart): + original_msg = msg + msg = MIMEMultipart('mixed') + msg['Subject'] = subject + # Format From header with name if provided + if self.config.name: + msg['From'] = formataddr((self.config.name, self.config.email)) + else: + msg['From'] = self.config.email + msg['To'] = to + if cc: + msg['Cc'] = cc + if bcc: + msg['Bcc'] = bcc + msg.attach(original_msg) + + for file_path in attachments: + self._attach_file(msg, file_path) + + # Prepare recipient list + recipients = [addr.strip() for addr in to.split(',')] + if cc: + recipients.extend([addr.strip() for addr in cc.split(',')]) + if bcc: + recipients.extend([addr.strip() for addr in bcc.split(',')]) + + # Send email + self.connection.send_message(msg, to_addrs=recipients) + logging.info(f"Email sent successfully to {to}") + + # Return success and the complete message for saving to Sent folder + return True, msg.as_string() + + except Exception as e: + logging.error(f"Error sending email: {str(e)}") + raise SendEmailError(f"Failed to send email: {str(e)}") + + def _attach_file(self, msg: MIMEMultipart, file_path: str): + """Attach file to message""" + # Validate file path + valid, error = validate_file_path(file_path, must_exist=True) + if not valid: + raise SendEmailError(f"Attachment error: {error}") + + # Validate attachment upload path if configured + config_manager = ConfigManager() + path_valid, path_error = config_manager.validate_attachment_upload_path(file_path) + if not path_valid: + raise SendEmailError(f"Attachment path validation failed: {path_error}") + + try: + with open(file_path, 'rb') as f: + attachment_data = f.read() + + # Create attachment + attachment = MIMEBase('application', 'octet-stream') + attachment.set_payload(attachment_data) + encoders.encode_base64(attachment) + + # Add header + filename = os.path.basename(file_path) + attachment.add_header( + 'Content-Disposition', + f'attachment; filename= {filename}' + ) + + msg.attach(attachment) + logging.debug(f"Attached file: {filename}") + + except Exception as e: + raise SendEmailError(f"Failed to attach file {file_path}: {str(e)}") + + def test_connection(self) -> bool: + """Test SMTP connection without sending email""" + try: + self.ensure_connected() + return True + except Exception: + return False \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/__init__.py new file mode 100644 index 00000000..d2423dc9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/__init__.py @@ -0,0 +1,3 @@ +from .settings import ConfigManager, config_manager + +__all__ = ['ConfigManager', 'config_manager'] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/settings.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/settings.py new file mode 100644 index 00000000..7a1834ef --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/settings.py @@ -0,0 +1,155 @@ +import json +import os +from pathlib import Path +from typing import Optional, List +from ..models.config import EmailConfig, WorkspaceConfig + + +class ConfigManager: + """Configuration manager for email MCP server""" + + def __init__(self): + self.workspace_config: Optional[WorkspaceConfig] = None + self.email_config: Optional[EmailConfig] = None + + def load_workspace_config(self, attachment_upload_path: str = None, + attachment_download_path: str = None, + email_export_path: str = None, + config_file: str = None) -> WorkspaceConfig: + """Load workspace configuration""" + self.workspace_config = WorkspaceConfig( + attachment_upload_path=attachment_upload_path, + attachment_download_path=attachment_download_path, + email_export_path=email_export_path, + config_file=config_file + ) + return self.workspace_config + + def load_email_config(self, config_file: str) -> EmailConfig: + """Load email configuration from JSON file (uses first account)""" + if not os.path.exists(config_file): + raise FileNotFoundError(f"Configuration file not found: {config_file}") + + try: + with open(config_file, 'r', encoding='utf-8') as f: + config_data = json.load(f) + + # if not isinstance(config_data, list): + # raise ValueError("Configuration file should contain a list of email accounts") + + if not config_data: + raise ValueError("Configuration file is empty") + + # Use first account only + if isinstance(config_data, list): + if not config_data: + raise ValueError("Configuration file is empty") + account_data = config_data[0] # Use first account + else: + account_data = config_data # Single account format + + if not isinstance(account_data, dict): + raise ValueError("Invalid account data format") + + email_config = EmailConfig( + email=account_data.get('email', ''), + password=account_data.get('password', ''), + name=account_data.get('name', ''), + imap_server=account_data.get('imap_server', 'localhost'), + imap_port=account_data.get('imap_port', 993), + smtp_server=account_data.get('smtp_server', 'localhost'), + smtp_port=account_data.get('smtp_port', 587), + use_ssl=account_data.get('use_ssl', True), + use_starttls=account_data.get('use_starttls', True) + ) + + # Validate required fields + if not email_config.email or not email_config.password: + raise ValueError("Email and password are required") + + self.email_config = email_config + return email_config + + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in configuration file: {str(e)}") + except Exception as e: + raise RuntimeError(f"Failed to load email configuration: {str(e)}") + + def get_email_config(self) -> Optional[EmailConfig]: + """Get email configuration""" + return self.email_config + + def validate_attachment_upload_path(self, file_path: str) -> tuple[bool, str]: + """Validate if file path is within attachment upload path""" + if not self.workspace_config or not self.workspace_config.attachment_upload_path: + return True, "" + + try: + upload_path = Path(self.workspace_config.attachment_upload_path).resolve() + resolved_path = Path(file_path).resolve() + + if not resolved_path.is_relative_to(upload_path): + return False, f"Error: Path '{file_path}' is outside attachment upload path '{upload_path}'" + return True, "" + + except Exception as e: + return False, f"Error validating attachment upload path: {str(e)}" + + def validate_attachment_download_path(self, file_path: str) -> tuple[bool, str]: + """Validate if file path is within attachment download path""" + if not self.workspace_config or not self.workspace_config.attachment_download_path: + return True, "" + + try: + download_path = Path(self.workspace_config.attachment_download_path).resolve() + resolved_path = Path(file_path).resolve() + + if not resolved_path.is_relative_to(download_path): + return False, f"Error: Path '{file_path}' is outside attachment download path '{download_path}'" + return True, "" + + except Exception as e: + return False, f"Error validating attachment download path: {str(e)}" + + def validate_email_export_path(self, file_path: str) -> tuple[bool, str]: + """Validate if file path is within email export path""" + if not self.workspace_config or not self.workspace_config.email_export_path: + return True, "" + + try: + export_path = Path(self.workspace_config.email_export_path).resolve() + resolved_path = Path(file_path).resolve() + + if not resolved_path.is_relative_to(export_path): + return False, f"Error: Path '{file_path}' is outside email export path '{export_path}'" + return True, "" + + except Exception as e: + return False, f"Error validating email export path: {str(e)}" + + def get_unique_download_path(self, filename: str) -> str: + """Get unique path for downloading file, adding (1), (2), etc. if file exists""" + if not self.workspace_config or not self.workspace_config.attachment_download_path: + return filename + + base_path = Path(self.workspace_config.attachment_download_path) + file_path = base_path / filename + + if not file_path.exists(): + return str(file_path) + + # Extract name and extension + stem = file_path.stem + suffix = file_path.suffix + + counter = 1 + while True: + new_filename = f"{stem}({counter}){suffix}" + new_path = base_path / new_filename + if not new_path.exists(): + return str(new_path) + counter += 1 + + +# Global config manager instance +config_manager = ConfigManager() \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/__init__.py new file mode 100644 index 00000000..15d0f07c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/__init__.py @@ -0,0 +1,12 @@ +from .config import EmailConfig, WorkspaceConfig +from .email import EmailMessage, EmailAttachment, EmailFolder, SearchResult, MailboxStats + +__all__ = [ + 'EmailConfig', + 'WorkspaceConfig', + 'EmailMessage', + 'EmailAttachment', + 'EmailFolder', + 'SearchResult', + 'MailboxStats' +] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/config.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/config.py new file mode 100644 index 00000000..88be16cf --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/config.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime + + +@dataclass +class EmailConfig: + """Email server configuration""" + email: str + password: str + name: str = "" + imap_server: str = "localhost" + imap_port: int = 993 + smtp_server: str = "localhost" + smtp_port: int = 587 + use_ssl: bool = True + use_starttls: bool = True + + +@dataclass +class WorkspaceConfig: + """Workspace configuration""" + attachment_upload_path: Optional[str] = None # Path for uploading attachments + attachment_download_path: Optional[str] = None # Path for downloading attachments + email_export_path: Optional[str] = None # Path for exporting emails + config_file: Optional[str] = None + max_page_size: int = 50 + default_page_size: int = 20 + connection_timeout: int = 30 + cache_timeout_minutes: int = 30 \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/email.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/email.py new file mode 100644 index 00000000..7ad530b7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/email.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass +from typing import List, Optional, Any + + +@dataclass +class EmailAttachment: + """Email attachment information""" + filename: str + content_type: str + size: int + attachment_id: Optional[str] = None + content: Optional[bytes] = None # 附件的实际内容数据 + + +@dataclass +class EmailMessage: + """Email message data model""" + email_id: str + subject: str + from_addr: str + to_addr: str + cc_addr: Optional[str] = None + bcc_addr: Optional[str] = None + date: Optional[str] = None + message_id: Optional[str] = None + body_text: Optional[str] = None + body_html: Optional[str] = None + attachments: List[EmailAttachment] = None + is_read: bool = False + is_important: bool = False + folder: Optional[str] = None + raw_message: Optional[Any] = None + + def __post_init__(self): + if self.attachments is None: + self.attachments = [] + + +@dataclass +class EmailFolder: + """Email folder information""" + name: str + total_messages: int = 0 + unread_messages: int = 0 + can_select: bool = True + + +@dataclass +class SearchResult: + """Email search result""" + emails: List[EmailMessage] + total_results: int + current_page: int + page_size: int + query: str + folder: Optional[str] = None + + @property + def total_pages(self) -> int: + return (self.total_results + self.page_size - 1) // self.page_size + + +@dataclass +class MailboxStats: + """Mailbox statistics""" + folder_name: str + total_messages: int + unread_messages: int + total_size_mb: Optional[float] = None \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/server.py new file mode 100644 index 00000000..3e0d72da --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/server.py @@ -0,0 +1,208 @@ +import argparse +import json +import logging +import sys +import os +import tempfile +from mcp.server.fastmcp import FastMCP +from .config import config_manager +from .services import EmailService, FolderService, SearchService, DraftService +from .backends.pg_backend import PgIMAPBackend, PgSMTPBackend, PgDraftBackend +from .backends import FileBackend +from .models.config import EmailConfig +from .tools import register_email_tools, register_folder_tools, register_management_tools + + +def setup_logging(debug: bool = False): + """Setup logging configuration""" + level = logging.DEBUG if debug else logging.INFO + logging.basicConfig( + level=level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stderr) + ] + ) + + +def _make_default_email_config() -> EmailConfig: + """Create a default EmailConfig for PG mode (no real IMAP/SMTP needed).""" + return EmailConfig( + email=os.environ.get("EMAIL_ADDRESS", "user@example.com"), + name=os.environ.get("EMAIL_NAME", "PG Email User"), + imap_server="localhost", + imap_port=993, + smtp_server="localhost", + smtp_port=587, + password="unused", + ) + + +def create_services(email_config): + """Create service instances using PostgreSQL backends""" + # Create PG-backed backends + imap_backend = PgIMAPBackend(email_config) + smtp_backend = PgSMTPBackend(email_config) + + email_export_path = config_manager.workspace_config.email_export_path if config_manager.workspace_config else None + attachment_download_path = config_manager.workspace_config.attachment_download_path if config_manager.workspace_config else None + file_backend = FileBackend(email_export_path, attachment_download_path) + + # Create services, injecting PG backends into EmailService + email_service = EmailService(email_config) + # Replace the IMAP/SMTP backends that EmailService created internally + email_service.imap_backend = imap_backend + email_service.smtp_backend = smtp_backend + + folder_service = FolderService(imap_backend) + search_service = SearchService(imap_backend) + draft_service = DraftService(file_backend) + + return email_service, folder_service, search_service, draft_service + + +def main(): + """Main function to run the emails MCP server""" + parser = argparse.ArgumentParser(description='Emails MCP Server') + parser.add_argument( + '--attachment_upload_path', + type=str, + default=None, + help='Directory path for attachment uploads (restricts file selection to this path and subdirectories)' + ) + parser.add_argument( + '--attachment_download_path', + type=str, + default=None, + help='Directory path for attachment downloads (files will be saved here with unique names)' + ) + parser.add_argument( + '--email_export_path', + type=str, + default=None, + help='Directory path for email exports (exports will be saved here with date-based filenames)' + ) + parser.add_argument( + '--config_file', + type=str, + default=None, + help='Email configuration file path (optional for PG mode)' + ) + parser.add_argument( + '--debug', + action='store_true', + help='Enable debug logging' + ) + + args = parser.parse_args() + + # Setup logging + setup_logging(args.debug) + logger = logging.getLogger(__name__) + + try: + # Initialize MCP server + mcp = FastMCP("emails-mcp") + + # If no config file provided or it doesn't exist, create a temporary + # dummy config so that config_manager doesn't complain. + config_file = args.config_file + _temp_config_file = None + + if config_file and os.path.exists(config_file): + # Real config file supplied – use it normally + pass + else: + if config_file and not os.path.exists(config_file): + logger.info( + f"Config file '{config_file}' not found – running in PG-only mode" + ) + else: + logger.info("No config file specified – running in PG-only mode") + + # Write a minimal dummy config so config_manager.load_email_config works + dummy = { + "email": os.environ.get("EMAIL_ADDRESS", "user@example.com"), + "name": os.environ.get("EMAIL_NAME", "PG Email User"), + "imap_server": "localhost", + "imap_port": 993, + "smtp_server": "localhost", + "smtp_port": 587, + "password": "unused", + } + fd, _temp_config_file = tempfile.mkstemp(suffix=".json", prefix="email_cfg_") + with os.fdopen(fd, "w") as fh: + json.dump(dummy, fh) + config_file = _temp_config_file + + # Load configuration + config_manager.load_workspace_config( + attachment_upload_path=args.attachment_upload_path, + attachment_download_path=args.attachment_download_path, + email_export_path=args.email_export_path, + config_file=config_file + ) + + email_config = config_manager.load_email_config(config_file) + if not email_config: + # Fallback: build a default config for PG mode + email_config = _make_default_email_config() + + logger.info(f"Loaded configuration for: {email_config.email}") + + # Create services (PG-backed) + email_service, folder_service, search_service, draft_service = create_services(email_config) + + # Register MCP tools + register_email_tools(mcp, email_service) + register_folder_tools(mcp, folder_service) + register_management_tools(mcp, draft_service, email_service) + + logger.info("All MCP tools registered successfully") + + # Log path restrictions if set + if config_manager.workspace_config: + config = config_manager.workspace_config + if config.attachment_upload_path: + logger.info(f"Attachment uploads restricted to: {config.attachment_upload_path}") + if config.attachment_download_path: + logger.info(f"Attachment downloads will be saved to: {config.attachment_download_path}") + if config.email_export_path: + logger.info(f"Email exports will be saved to: {config.email_export_path}") + + # Test PG connection on startup + try: + imap_ok, smtp_ok = email_service.check_connection() + if imap_ok and smtp_ok: + logger.info("All PG email connections verified successfully") + else: + logger.warning("PG email connection check returned partial failure") + except Exception as e: + logger.warning(f"PG connection test failed: {str(e)}") + + # Start the MCP server + logger.info("Starting emails MCP server (PG backend)...") + mcp.run(transport='stdio') + + except KeyboardInterrupt: + logger.info("Server shutdown requested") + except Exception as e: + logger.error(f"Server startup failed: {str(e)}") + sys.exit(1) + finally: + # Cleanup + try: + if 'email_service' in locals(): + email_service.cleanup() + except: + pass + # Remove temporary config file if we created one + try: + if '_temp_config_file' in locals() and _temp_config_file and os.path.exists(_temp_config_file): + os.unlink(_temp_config_file) + except: + pass + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/__init__.py new file mode 100644 index 00000000..fcf6aa2e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/__init__.py @@ -0,0 +1,6 @@ +from .email_service import EmailService +from .folder_service import FolderService +from .search_service import SearchService +from .draft_service import DraftService + +__all__ = ['EmailService', 'FolderService', 'SearchService', 'DraftService'] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/draft_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/draft_service.py new file mode 100644 index 00000000..b9b3d9d6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/draft_service.py @@ -0,0 +1,172 @@ +from typing import List, Optional, Dict, Any +from datetime import datetime +from ..models.email import EmailMessage +from ..backends.file_backend import FileBackend +from ..utils.exceptions import EmailMCPError + + +class DraftService: + """Draft management service layer""" + + def __init__(self, file_backend: FileBackend): + self.file_backend = file_backend + self.drafts: Dict[str, Dict[str, Any]] = {} + self._draft_counter = 1 + + def save_draft(self, subject: str, body: str, + html_body: Optional[str] = None, + to: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None) -> str: + """Save email draft and return draft ID""" + try: + draft_id = f"draft_{self._draft_counter}" + self._draft_counter += 1 + + draft_data = { + 'draft_id': draft_id, + 'subject': subject, + 'body': body, + 'html_body': html_body, + 'to': to, + 'cc': cc, + 'bcc': bcc, + 'created_at': datetime.now().isoformat(), + 'updated_at': datetime.now().isoformat() + } + + self.drafts[draft_id] = draft_data + return draft_id + + except Exception as e: + raise EmailMCPError(f"Failed to save draft: {str(e)}") + + def get_drafts(self, page: int = 1, page_size: int = 20) -> Dict[str, Any]: + """Get paginated list of drafts""" + try: + draft_list = list(self.drafts.values()) + draft_list.sort(key=lambda x: x['updated_at'], reverse=True) + + total_drafts = len(draft_list) + total_pages = (total_drafts + page_size - 1) // page_size if total_drafts > 0 else 1 + + if page < 1: + page = 1 + elif page > total_pages: + page = total_pages + + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + page_drafts = draft_list[start_idx:end_idx] + + return { + 'drafts': page_drafts, + 'total_drafts': total_drafts, + 'current_page': page, + 'total_pages': total_pages, + 'page_size': page_size + } + + except Exception as e: + raise EmailMCPError(f"Failed to get drafts: {str(e)}") + + def get_draft(self, draft_id: str) -> Dict[str, Any]: + """Get specific draft by ID""" + if draft_id not in self.drafts: + raise EmailMCPError(f"Draft not found: {draft_id}") + + return self.drafts[draft_id] + + def update_draft(self, draft_id: str, + subject: Optional[str] = None, + body: Optional[str] = None, + html_body: Optional[str] = None, + to: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None) -> bool: + """Update existing draft""" + if draft_id not in self.drafts: + raise EmailMCPError(f"Draft not found: {draft_id}") + + try: + draft = self.drafts[draft_id] + + # Update only provided fields + if subject is not None: + draft['subject'] = subject + if body is not None: + draft['body'] = body + if html_body is not None: + draft['html_body'] = html_body + if to is not None: + draft['to'] = to + if cc is not None: + draft['cc'] = cc + if bcc is not None: + draft['bcc'] = bcc + + draft['updated_at'] = datetime.now().isoformat() + + return True + + except Exception as e: + raise EmailMCPError(f"Failed to update draft: {str(e)}") + + def delete_draft(self, draft_id: str) -> bool: + """Delete draft""" + if draft_id not in self.drafts: + raise EmailMCPError(f"Draft not found: {draft_id}") + + try: + del self.drafts[draft_id] + return True + except Exception as e: + raise EmailMCPError(f"Failed to delete draft: {str(e)}") + + def export_drafts(self, export_path: str) -> bool: + """Export all drafts to file""" + try: + # Convert drafts to list for export + draft_emails = [] + for draft_data in self.drafts.values(): + # Create minimal EmailMessage for export + email_obj = EmailMessage( + email_id=draft_data['draft_id'], + subject=draft_data['subject'], + from_addr="", # Will be set when sending + to_addr=draft_data.get('to', ''), + cc_addr=draft_data.get('cc'), + bcc_addr=draft_data.get('bcc'), + body_text=draft_data['body'], + body_html=draft_data.get('html_body'), + folder="Drafts" + ) + draft_emails.append(email_obj) + + return self.file_backend.export_emails(draft_emails, export_path, 'json') + + except Exception as e: + raise EmailMCPError(f"Failed to export drafts: {str(e)}") + + def import_drafts(self, import_path: str) -> int: + """Import drafts from file""" + try: + imported_emails = self.file_backend.import_emails(import_path) + imported_count = 0 + + for email_obj in imported_emails: + # Convert EmailMessage back to draft format + self.save_draft( + subject=email_obj.subject, + body=email_obj.body_text or "", + html_body=email_obj.body_html, + to=email_obj.to_addr, + cc=email_obj.cc_addr, + bcc=email_obj.bcc_addr + ) + imported_count += 1 + + return imported_count + + except Exception as e: + raise EmailMCPError(f"Failed to import drafts: {str(e)}") \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/email_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/email_service.py new file mode 100644 index 00000000..7cf4a692 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/email_service.py @@ -0,0 +1,433 @@ +from typing import List, Optional, Tuple +from datetime import datetime +import logging +from ..models.config import EmailConfig +from ..models.email import EmailMessage, SearchResult +from ..backends.pg_backend import PgIMAPBackend, PgSMTPBackend +from ..utils.exceptions import EmailMCPError, ValidationError +from ..utils.validators import validate_page_params, validate_search_query +from ..utils.email_parser import format_email_summary + + +class EmailService: + """Email operations service layer""" + + def __init__(self, email_config: EmailConfig): + self.config = email_config + self.imap_backend = PgIMAPBackend(email_config) + self.smtp_backend = PgSMTPBackend(email_config) + + def get_emails(self, folder: str = "INBOX", page: int = 1, page_size: int = 20) -> SearchResult: + """Get paginated emails from folder""" + try: + # Validate parameters + page, page_size, warning = validate_page_params(page, page_size) + + # Get total count and select folder + total_messages, unread_count = self.imap_backend.select_folder(folder) + + if total_messages == 0: + return SearchResult( + emails=[], + total_results=0, + current_page=1, + page_size=page_size, + query="", + folder=folder + ) + + # Calculate pagination + total_pages = (total_messages + page_size - 1) // page_size + if page > total_pages: + page = total_pages + + # Get email IDs for current page + start_idx = (page - 1) * page_size + email_ids = self.imap_backend.get_email_ids(folder, limit=total_messages) + page_ids = email_ids[start_idx:start_idx + page_size] + + # Fetch emails + emails = [] + for email_id in page_ids: + try: + email_obj = self.imap_backend.fetch_email(email_id) + emails.append(email_obj) + except Exception as e: + # Log error but continue with other emails + import logging + logging.error(f"Failed to fetch email {email_id}: {str(e)}") + + result = SearchResult( + emails=emails, + total_results=total_messages, + current_page=page, + page_size=page_size, + query="", + folder=folder + ) + + return result + + except Exception as e: + raise EmailMCPError(f"Failed to get emails: {str(e)}") + + def read_email(self, email_id: str) -> EmailMessage: + """Read specific email by ID""" + try: + email_obj = self.imap_backend.fetch_email(email_id) + + # Mark as read + if self.imap_backend.mark_as_read(email_id): + email_obj.is_read = True + else: + logging.warning(f"Failed to mark email {email_id} as read") + # Continue anyway, as the email content was retrieved successfully + + return email_obj + + except Exception as e: + raise EmailMCPError(f"Failed to read email {email_id}: {str(e)}") + + def search_emails(self, query: str, folder: Optional[str] = None, + page: int = 1, page_size: int = 20) -> SearchResult: + """Search emails with pagination""" + try: + # Validate query + valid, error = validate_search_query(query) + if not valid: + raise ValidationError(error) + + # Validate parameters + page, page_size, warning = validate_page_params(page, page_size) + + # Search emails + email_ids = self.imap_backend.search_emails(query, folder) + total_results = len(email_ids) + + if total_results == 0: + return SearchResult( + emails=[], + total_results=0, + current_page=1, + page_size=page_size, + query=query, + folder=folder + ) + + # Calculate pagination + total_pages = (total_results + page_size - 1) // page_size + if page > total_pages: + page = total_pages + + # Get page slice + start_idx = (page - 1) * page_size + page_ids = email_ids[start_idx:start_idx + page_size] + + # Fetch emails + emails = [] + for email_id in page_ids: + try: + email_obj = self.imap_backend.fetch_email(email_id) + emails.append(email_obj) + except Exception as e: + import logging + logging.error(f"Failed to fetch search result {email_id}: {str(e)}") + + return SearchResult( + emails=emails, + total_results=total_results, + current_page=page, + page_size=page_size, + query=query, + folder=folder + ) + + except Exception as e: + raise EmailMCPError(f"Failed to search emails: {str(e)}") + + def send_email(self, to: str, subject: str, body: str, + html_body: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None, + attachments: Optional[List[str]] = None, + save_to_sent: bool = True) -> bool: + """Send email and optionally save to Sent folder""" + try: + # Send the email first + success, message_string = self.smtp_backend.send_email( + to=to, + subject=subject, + body=body, + html_body=html_body, + cc=cc, + bcc=bcc, + attachments=attachments + ) + + # If sending was successful and save_to_sent is True, save to Sent folder + # Skip if using PgSMTPBackend — it already saves to Sent internally + from emails_mcp.backends.pg_backend import PgSMTPBackend + is_pg = isinstance(self.smtp_backend, PgSMTPBackend) + if success and save_to_sent and message_string and not is_pg: + try: + # Try common sent folder names + sent_folders = ["Sent", "INBOX.Sent", "Sent Messages", "Sent Items"] + saved = False + + for folder in sent_folders: + try: + self.imap_backend.append_message(folder, message_string) + saved = True + logging.info(f"Email saved to {folder} folder") + break + except Exception as e: + logging.debug(f"Failed to save to {folder}: {str(e)}") + continue + + if not saved: + logging.warning("Could not save email to any Sent folder") + + except Exception as e: + logging.error(f"Error saving email to Sent folder: {str(e)}") + # Don't fail the whole operation if saving to Sent fails + + return success + + except Exception as e: + raise EmailMCPError(f"Failed to send email: {str(e)}") + + def reply_email(self, email_id: str, body: str, + html_body: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None, + reply_all: bool = False) -> bool: + """Reply to email""" + try: + # Get original email + original_email = self.imap_backend.fetch_email(email_id) + + # Prepare reply subject + original_subject = original_email.subject or "" + reply_subject = f"Re: {original_subject}" if not original_subject.startswith('Re:') else original_subject + + # Determine recipients + reply_to = original_email.from_addr + + if reply_all: + # Include original TO and CC recipients (excluding ourselves) + all_recipients = [] + if original_email.to_addr: + all_recipients.extend([addr.strip() for addr in original_email.to_addr.split(',')]) + if original_email.cc_addr: + all_recipients.extend([addr.strip() for addr in original_email.cc_addr.split(',')]) + + # Remove our own email (handle both "email" and "Name " formats) + our_email = self.config.email.lower() + all_recipients = [addr for addr in all_recipients + if our_email not in addr.lower()] + reply_cc = ','.join(all_recipients) if all_recipients else cc + else: + reply_cc = cc + + # Prepare body with original message + original_body = original_email.body_text or "" + full_body = f"{body}\n\n--- Original Message ---\nFrom: {original_email.from_addr}\nDate: {original_email.date}\nSubject: {original_subject}\n\n{original_body}" + + # Prepare HTML body if provided + full_html_body = None + if html_body: + original_html = original_email.body_html or original_body + full_html_body = f"{html_body}


Original Message:
From: {original_email.from_addr}
Date: {original_email.date}
Subject: {original_subject}

{original_html}" + + # Send reply + return self.send_email( + to=reply_to, + subject=reply_subject, + body=full_body, + html_body=full_html_body, + cc=reply_cc, + bcc=bcc + ) + + except Exception as e: + raise EmailMCPError(f"Failed to reply to email: {str(e)}") + + def forward_email(self, email_id: str, to: str, + body: Optional[str] = None, + html_body: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None) -> bool: + """Forward email with attachments""" + try: + # Get original email + original_email = self.imap_backend.fetch_email(email_id) + + # Prepare forward subject + original_subject = original_email.subject or "" + forward_subject = f"Fwd: {original_subject}" if not original_subject.startswith('Fwd:') else original_subject + + # Prepare body with forwarded content + original_body = original_email.body_text or "" + forward_body = f"{body or ''}\n\n--- Forwarded Message ---\nFrom: {original_email.from_addr}\nTo: {original_email.to_addr}\nDate: {original_email.date}\nSubject: {original_subject}\n\n{original_body}" + + # Prepare HTML body if provided + forward_html_body = None + if html_body or original_email.body_html: + original_html = original_email.body_html or original_body + forward_html_body = f"{html_body or ''}


Forwarded Message:
From: {original_email.from_addr}
To: {original_email.to_addr}
Date: {original_email.date}
Subject: {original_subject}

{original_html}" + + # Forward with attachments from the original email + return self._send_with_original_attachments( + to=to, + subject=forward_subject, + body=forward_body, + html_body=forward_html_body, + cc=cc, + bcc=bcc, + original_email=original_email + ) + + except Exception as e: + raise EmailMCPError(f"Failed to forward email: {str(e)}") + + def _send_with_original_attachments(self, to: str, subject: str, body: str, + html_body: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None, + original_email: EmailMessage = None) -> bool: + """Send email with attachments from original email""" + try: + if not original_email or not original_email.attachments: + # No attachments, use regular send_email + return self.send_email( + to=to, + subject=subject, + body=body, + html_body=html_body, + cc=cc, + bcc=bcc + ) + + # Extract attachment data from original email's raw message + import tempfile + import os + temp_files = [] + + try: + if original_email.raw_message: + for part in original_email.raw_message.walk(): + if part.get_content_disposition() == 'attachment': + filename = part.get_filename() + if filename: + # Decode attachment data + attachment_data = part.get_payload(decode=True) + if attachment_data: + # Create temporary file with original filename + import tempfile + temp_dir = tempfile.mkdtemp() + temp_file_path = os.path.join(temp_dir, filename) + with open(temp_file_path, 'wb') as f: + f.write(attachment_data) + temp_files.append(temp_file_path) + + # Send email with temporary attachment files + success = self.send_email( + to=to, + subject=subject, + body=body, + html_body=html_body, + cc=cc, + bcc=bcc, + attachments=temp_files + ) + + return success + + finally: + # Clean up temporary files and directories + for temp_file_path in temp_files: + try: + os.unlink(temp_file_path) + # Also remove the temporary directory if it's empty + temp_dir = os.path.dirname(temp_file_path) + try: + os.rmdir(temp_dir) + except OSError: + pass # Directory not empty or already removed + except: + pass + + except Exception as e: + raise EmailMCPError(f"Failed to send email with original attachments: {str(e)}") + + def _check_email_exists(self, email_id: str) -> bool: + """Check if email exists in the database""" + try: + self.imap_backend.fetch_email(email_id) + return True + except Exception: + return False + + def delete_email(self, email_id: str) -> bool: + """Delete email""" + try: + self.imap_backend.delete_email(email_id) + return True + except Exception as e: + raise EmailMCPError(f"Failed to delete email: {str(e)}") + + def move_email(self, email_id: str, target_folder: str) -> bool: + """Move email to another folder""" + try: + self.imap_backend.move_email(email_id, target_folder) + return True + except Exception as e: + raise EmailMCPError(f"Failed to move email: {str(e)}") + + def mark_emails(self, email_ids: List[str], status: str) -> int: + """Mark multiple emails with status (read/unread/important)""" + success_count = 0 + + for email_id in email_ids: + try: + success = False + if status == "read": + success = self.imap_backend.mark_as_read(email_id) + elif status == "unread": + success = self.imap_backend.mark_as_unread(email_id) + elif status == "important": + success = self.imap_backend.mark_as_important(email_id) + elif status == "not_important": + success = self.imap_backend.mark_as_not_important(email_id) + + if success: + success_count += 1 + else: + logging.warning(f"Failed to mark email {email_id} as {status}") + except Exception as e: + logging.error(f"Failed to mark email {email_id} as {status}: {str(e)}") + + return success_count + + def check_connection(self) -> Tuple[bool, bool]: + """Check IMAP and SMTP connections""" + imap_ok = False + smtp_ok = False + + try: + self.imap_backend.ensure_connected() + imap_ok = True + except: + pass + + try: + smtp_ok = self.smtp_backend.test_connection() + except: + pass + + return imap_ok, smtp_ok + + def cleanup(self): + """Cleanup connections""" + self.imap_backend.disconnect() + self.smtp_backend.disconnect() \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/folder_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/folder_service.py new file mode 100644 index 00000000..70bbf62b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/folder_service.py @@ -0,0 +1,85 @@ +from typing import List +import logging +from ..models.email import EmailFolder, MailboxStats +from ..utils.exceptions import EmailMCPError, FolderError +from ..utils.validators import validate_folder_name + +class FolderService: + """Folder management service layer""" + + def __init__(self, imap_backend): + self.imap_backend = imap_backend + + def get_folders(self) -> List[EmailFolder]: + """Get list of all email folders""" + try: + return self.imap_backend.list_folders() + except Exception as e: + raise EmailMCPError(f"Failed to get folders: {str(e)}") + + def create_folder(self, folder_name: str) -> bool: + """Create new email folder""" + # Validate folder name + valid, error = validate_folder_name(folder_name) + if not valid: + raise FolderError(error) + + try: + return self.imap_backend.create_folder(folder_name) + except FolderError: + raise + except Exception as e: + raise FolderError(f"Failed to create folder: {str(e)}") + + def delete_folder(self, folder_name: str) -> bool: + """Delete email folder""" + # Validate folder name + valid, error = validate_folder_name(folder_name) + if not valid: + raise FolderError(error) + + # Prevent deletion of system folders + system_folders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Spam'] + if folder_name.strip() in system_folders: + raise FolderError(f"Cannot delete system folder: {folder_name}") + + try: + return self.imap_backend.delete_folder(folder_name) + except FolderError: + raise + except Exception as e: + raise FolderError(f"Failed to delete folder: {str(e)}") + + def get_folder_stats(self, folder_name: str) -> MailboxStats: + """Get statistics for specific folder""" + try: + folder_name = folder_name.strip() + total_messages, unread_messages = self.imap_backend.select_folder(folder_name) + + return MailboxStats( + folder_name=folder_name, + total_messages=total_messages, + unread_messages=unread_messages + ) + + except Exception as e: + raise EmailMCPError(f"Failed to get folder stats: {str(e)}") + + def get_unread_count(self, folder_name: str = None) -> int: + """Get unread message count for folder or all folders""" + try: + if folder_name: + folder_name = folder_name.strip() + _, unread_count = self.imap_backend.select_folder(folder_name) + return unread_count + else: + # Get unread count for all folders + folders = self.get_folders() + total_unread = 0 + for folder in folders: + if folder.can_select: + total_unread += folder.unread_messages + return total_unread + + except Exception as e: + raise EmailMCPError(f"Failed to get unread count: {str(e)}") \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/search_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/search_service.py new file mode 100644 index 00000000..f1fd8a07 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/search_service.py @@ -0,0 +1,84 @@ +import logging +from typing import List, Optional +from ..models.email import EmailMessage +from ..utils.exceptions import EmailMCPError + + +class SearchService: + """Email search service layer""" + + def __init__(self, imap_backend): + self.imap_backend = imap_backend + + def search_emails_by_query(self, query: str, folder: Optional[str] = None) -> List[str]: + """Search emails and return email IDs""" + try: + # If no folder specified, use INBOX as default + if not folder: + folder = 'INBOX' + return self.imap_backend.search_emails(query, folder) + except Exception as e: + raise EmailMCPError(f"Failed to search emails: {str(e)}") + + def search_by_sender(self, sender: str, folder: Optional[str] = None) -> List[str]: + """Search emails by sender using PG full-text search on from_addr""" + try: + if not folder: + folder = 'INBOX' + # Delegate to the backend's search which searches subject+body. + # For sender-specific search we query the DB directly. + self.imap_backend.ensure_connected() + folder_id = self.imap_backend._get_or_create_folder(folder) + with self.imap_backend.connection.cursor() as cur: + cur.execute( + "SELECT id FROM email.messages " + "WHERE folder_id = %s AND from_addr ILIKE %s " + "ORDER BY date DESC, id DESC", + (folder_id, f"%{sender}%"), + ) + rows = cur.fetchall() + return [str(r[0]) for r in rows] + except Exception as e: + raise EmailMCPError(f"Failed to search by sender: {str(e)}") + + def search_by_subject(self, subject: str, folder: Optional[str] = None) -> List[str]: + """Search emails by subject using PG ILIKE""" + try: + if not folder: + folder = 'INBOX' + self.imap_backend.ensure_connected() + folder_id = self.imap_backend._get_or_create_folder(folder) + with self.imap_backend.connection.cursor() as cur: + cur.execute( + "SELECT id FROM email.messages " + "WHERE folder_id = %s AND subject ILIKE %s " + "ORDER BY date DESC, id DESC", + (folder_id, f"%{subject}%"), + ) + rows = cur.fetchall() + return [str(r[0]) for r in rows] + except Exception as e: + raise EmailMCPError(f"Failed to search by subject: {str(e)}") + + def search_by_date_range(self, since_date: str, before_date: Optional[str] = None, + folder: Optional[str] = None) -> List[str]: + """Search emails by date range (YYYY-MM-DD format)""" + try: + if not folder: + folder = 'INBOX' + self.imap_backend.ensure_connected() + folder_id = self.imap_backend._get_or_create_folder(folder) + + sql = "SELECT id FROM email.messages WHERE folder_id = %s AND date >= %s::date" + params: list = [folder_id, since_date] + if before_date: + sql += " AND date < %s::date" + params.append(before_date) + sql += " ORDER BY date DESC, id DESC" + + with self.imap_backend.connection.cursor() as cur: + cur.execute(sql, params) + rows = cur.fetchall() + return [str(r[0]) for r in rows] + except Exception as e: + raise EmailMCPError(f"Failed to search by date: {str(e)}") \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/__init__.py new file mode 100644 index 00000000..64456a07 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/__init__.py @@ -0,0 +1,5 @@ +from .email_tools import register_email_tools +from .folder_tools import register_folder_tools +from .management_tools import register_management_tools + +__all__ = ['register_email_tools', 'register_folder_tools', 'register_management_tools'] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/email_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/email_tools.py new file mode 100644 index 00000000..b162bf83 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/email_tools.py @@ -0,0 +1,382 @@ +import logging +from typing import List, Optional +from mcp.server.fastmcp import FastMCP +from ..services.email_service import EmailService +from ..utils.email_parser import format_email_summary + + +def register_email_tools(mcp: FastMCP, email_service: EmailService): + """Register email-related MCP tools""" + + @mcp.tool() + async def get_emails(folder: str = "INBOX", page: int = 1, page_size: int = 20) -> str: + """Get paginated list of emails from specified folder + + Args: + folder: Email folder name (default: INBOX) + page: Page number starting from 1 (default: 1) + page_size: Number of emails per page (default: 20) + """ + try: + result = email_service.get_emails(folder, page, page_size) + + if not result.emails: + return f"Folder '{folder}' is empty or page {page} is out of range" + + output = f"Folder: {folder}\n" + output += f"Page: {result.current_page}/{result.total_pages}\n" + output += f"Total emails: {result.total_results}\n\n" + + for i, email in enumerate(result.emails, 1): + output += f"{(result.current_page-1)*result.page_size + i}. " + output += f"ID: {email.email_id}\n" + output += f" Subject: {email.subject}\n" + output += f" From: {email.from_addr}\n" + output += f" Date: {email.date}\n" + if email.attachments: + output += f" Attachments: {len(email.attachments)} files\n" + output += "\n" + + return output + + except Exception as e: + return f"Error getting emails: {str(e)}" + + @mcp.tool() + async def read_email(email_id: str) -> str: + """Read full content of a specific email + + Args: + email_id: Email ID to read + """ + try: + email = email_service.read_email(email_id) + + output = f"Email ID: {email.email_id}\n" + output += f"Subject: {email.subject}\n" + output += f"From: {email.from_addr}\n" + output += f"To: {email.to_addr}\n" + + if email.cc_addr: + output += f"CC: {email.cc_addr}\n" + + output += f"Date: {email.date}\n" + output += f"Message-ID: {email.message_id}\n\n" + + if email.body_text: + output += "Text Content:\n" + output += f"{email.body_text}\n\n" + + if email.body_html: + output += "HTML Content:\n" + output += f"{email.body_html}\n\n" + + if email.attachments: + output += "Attachments:\n" + for i, att in enumerate(email.attachments, 1): + output += f"{i}. {att.filename} ({att.content_type}, {att.size} bytes)\n" + + return output + + except Exception as e: + return f"Error reading email: {str(e)}" + + @mcp.tool() + async def search_emails(query: str, folder: str = "INBOX", page: int = 1, page_size: int = 20) -> str: + """Search emails with query string (sorted by date descending) + + Args: + query: Search query (subject, from, body content) + folder: Folder to search in (default: INBOX) + page: Page number starting from 1 (default: 1) + page_size: Number of results per page (default: 20) + """ + try: + result = email_service.search_emails(query, folder, page, page_size) + + if not result.emails: + return f"No emails found matching query: {query}" + + output = f"Search query: {query}\n" + output += f"Folder: {result.folder or 'current'}\n" + output += f"Page: {result.current_page}/{result.total_pages}\n" + output += f"Total results: {result.total_results}\n\n" + + for i, email in enumerate(result.emails, 1): + output += f"{(result.current_page-1)*result.page_size + i}. " + output += f"ID: {email.email_id}\n" + output += f" Subject: {email.subject}\n" + output += f" From: {email.from_addr}\n" + output += f" Date: {email.date}\n\n" + + return output + + except Exception as e: + return f"Error searching emails: {str(e)}" + + @mcp.tool() + async def send_email(to: str, subject: str, body: str, html_body: str = None, + cc: str = None, bcc: str = None, attachments: List[str] = None) -> str: + """Send an email with optional HTML body, CC, BCC, and attachments + + Args: + to: Recipient email address(es), comma-separated + subject: Email subject + body: Plain text body + html_body: HTML body content (optional) + cc: CC recipients, comma-separated (optional) + bcc: BCC recipients, comma-separated (optional) + attachments: List of file paths to attach (optional) + """ + try: + success = email_service.send_email( + to=to, + subject=subject, + body=body, + html_body=html_body, + cc=cc, + bcc=bcc, + attachments=attachments + ) + + if success: + attachment_info = f" with {len(attachments)} attachments" if attachments else "" + return f"Email sent successfully to {to}{attachment_info}" + else: + return "Email sending failed" + + except Exception as e: + return f"Error sending email: {str(e)}" + + @mcp.tool() + async def reply_email(email_id: str, body: str, html_body: str = None, + cc: str = None, bcc: str = None, reply_all: bool = False) -> str: + """Reply to an email + + Args: + email_id: ID of email to reply to + body: Reply message body (plain text) + html_body: Reply message body (HTML, optional) + cc: Additional CC recipients (optional) + bcc: BCC recipients (optional) + reply_all: Whether to reply to all recipients (default: False) + """ + try: + success = email_service.reply_email( + email_id=email_id, + body=body, + html_body=html_body, + cc=cc, + bcc=bcc, + reply_all=reply_all + ) + + if success: + reply_type = "all recipients" if reply_all else "sender" + return f"Reply sent successfully to {reply_type}" + else: + return "Reply sending failed" + + except Exception as e: + return f"Error replying to email: {str(e)}" + + @mcp.tool() + async def forward_email(email_id: str, to: str, body: str = None, html_body: str = None, + cc: str = None, bcc: str = None) -> str: + """Forward an email to other recipients + + Args: + email_id: ID of email to forward + to: Recipients to forward to + body: Additional message body (optional) + html_body: Additional HTML message body (optional) + cc: CC recipients (optional) + bcc: BCC recipients (optional) + """ + try: + success = email_service.forward_email( + email_id=email_id, + to=to, + body=body, + html_body=html_body, + cc=cc, + bcc=bcc + ) + + if success: + return f"Email forwarded successfully to {to}" + else: + return "Email forwarding failed" + + except Exception as e: + return f"Error forwarding email: {str(e)}" + + @mcp.tool() + async def delete_email(email_id: str) -> str: + """Delete an email + + Args: + email_id: Email ID to delete + """ + try: + success = email_service.delete_email(email_id) + + if success: + return f"Email {email_id} deleted successfully" + else: + return "Email deletion failed" + + except Exception as e: + return f"Error deleting email: {str(e)}" + + @mcp.tool() + async def move_email(email_id: str, target_folder: str) -> str: + """Move email to another folder + + Args: + email_id: Email ID to move + target_folder: Target folder name + """ + try: + success = email_service.move_email(email_id, target_folder) + + if success: + return f"Email {email_id} moved to {target_folder} successfully" + else: + return "Email move failed" + + except Exception as e: + return f"Error moving email: {str(e)}" + + @mcp.tool() + async def mark_emails(email_ids: List[str], status: str) -> str: + """Mark multiple emails with status (read/unread/important/not_important) + + Args: + email_ids: List of email IDs to mark + status: Status to set (read, unread, important, not_important) + """ + try: + if status not in ['read', 'unread', 'important', 'not_important']: + return "Error: Status must be 'read', 'unread', 'important', or 'not_important'" + + success_count = email_service.mark_emails(email_ids, status) + total_count = len(email_ids) + + return f"Successfully marked {success_count}/{total_count} emails as {status}" + + except Exception as e: + return f"Error marking emails: {str(e)}" + + @mcp.tool() + async def move_emails(email_ids: List[str], target_folder: str) -> str: + """Move multiple emails to another folder with improved ID synchronization + + Args: + email_ids: List of email IDs to move + target_folder: Target folder name + """ + try: + success_count = 0 + failed_count = 0 + failed_ids = [] + + # Sort email IDs in descending order to avoid ID renumbering issues + # When emails are deleted/moved, higher IDs remain stable + sorted_ids = sorted(email_ids, key=lambda x: int(x) if x.isdigit() else 0, reverse=True) + + for email_id in sorted_ids: + try: + # Verify email exists before attempting move + email_exists = email_service._check_email_exists(email_id) + if not email_exists: + logging.warning(f"Email {email_id} no longer exists, skipping") + failed_count += 1 + failed_ids.append(email_id) + continue + + success = email_service.move_email(email_id, target_folder) + if success: + success_count += 1 + logging.info(f"Successfully moved email {email_id}") + else: + failed_count += 1 + failed_ids.append(email_id) + logging.warning(f"Failed to move email {email_id}") + + except Exception as e: + failed_count += 1 + failed_ids.append(email_id) + logging.error(f"Failed to move email {email_id}: {str(e)}") + + total_count = len(email_ids) + result_msg = f"Successfully moved {success_count}/{total_count} emails to {target_folder}" + + if failed_count > 0: + result_msg += f" ({failed_count} failed" + if failed_ids: + result_msg += f": {', '.join(failed_ids[:5])}" # Show first 5 failed IDs + if len(failed_ids) > 5: + result_msg += f" and {len(failed_ids)-5} more" + result_msg += ")" + + return result_msg + + except Exception as e: + return f"Error moving emails: {str(e)}" + + @mcp.tool() + async def delete_emails(email_ids: List[str]) -> str: + """Delete multiple emails with improved ID synchronization + + Args: + email_ids: List of email IDs to delete + """ + try: + success_count = 0 + failed_count = 0 + failed_ids = [] + + # Sort email IDs in descending order to avoid ID renumbering issues + # When emails are deleted, higher IDs remain stable + sorted_ids = sorted(email_ids, key=lambda x: int(x) if x.isdigit() else 0, reverse=True) + + for email_id in sorted_ids: + try: + # Verify email exists before attempting deletion + email_exists = email_service._check_email_exists(email_id) + if not email_exists: + logging.warning(f"Email {email_id} no longer exists, skipping") + failed_count += 1 + failed_ids.append(email_id) + continue + + success = email_service.delete_email(email_id) + if success: + success_count += 1 + logging.info(f"Successfully deleted email {email_id}") + else: + failed_count += 1 + failed_ids.append(email_id) + logging.warning(f"Failed to delete email {email_id}") + + except Exception as e: + failed_count += 1 + failed_ids.append(email_id) + logging.error(f"Failed to delete email {email_id}: {str(e)}") + + total_count = len(email_ids) + result_msg = f"Successfully deleted {success_count}/{total_count} emails" + + if failed_count > 0: + result_msg += f" ({failed_count} failed" + if failed_ids: + result_msg += f": {', '.join(failed_ids[:5])}" # Show first 5 failed IDs + if len(failed_ids) > 5: + result_msg += f" and {len(failed_ids)-5} more" + result_msg += ")" + + return result_msg + + except Exception as e: + return f"Error deleting emails: {str(e)}" \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/folder_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/folder_tools.py new file mode 100644 index 00000000..b9149433 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/folder_tools.py @@ -0,0 +1,117 @@ +from mcp.server.fastmcp import FastMCP +from ..services.folder_service import FolderService + + +def register_folder_tools(mcp: FastMCP, folder_service: FolderService): + """Register folder-related MCP tools""" + + @mcp.tool() + async def get_folders() -> str: + """Get list of available email folders""" + try: + folders = folder_service.get_folders() + + if not folders: + return "No folders found" + + output = "Available folders:\n" + for i, folder in enumerate(folders, 1): + output += f"{i}. {folder.name}" + if folder.can_select: + output += f" ({folder.total_messages} total, {folder.unread_messages} unread)" + else: + output += " (cannot select)" + output += "\n" + + return output + + except Exception as e: + return f"Error getting folders: {str(e)}" + + @mcp.tool() + async def create_folder(folder_name: str) -> str: + """Create new email folder + + Args: + folder_name: Name of folder to create + """ + try: + success = folder_service.create_folder(folder_name) + + if success: + return f"Folder '{folder_name}' created successfully" + else: + return f"Failed to create folder '{folder_name}'" + + except Exception as e: + return f"Error creating folder: {str(e)}" + + @mcp.tool() + async def delete_folder(folder_name: str) -> str: + """Delete email folder + + Args: + folder_name: Name of folder to delete + """ + try: + success = folder_service.delete_folder(folder_name) + + if success: + return f"Folder '{folder_name}' deleted successfully" + else: + return f"Failed to delete folder '{folder_name}'" + + except Exception as e: + return f"Error deleting folder: {str(e)}" + + @mcp.tool() + async def get_mailbox_stats(folder_name: str = None) -> str: + """Get mailbox statistics + + Args: + folder_name: Specific folder name (optional, defaults to all folders) + """ + try: + if folder_name: + stats = folder_service.get_folder_stats(folder_name) + output = f"Folder Statistics for '{stats.folder_name}':\n" + output += f"Total messages: {stats.total_messages}\n" + output += f"Unread messages: {stats.unread_messages}\n" + if stats.total_size_mb: + output += f"Total size: {stats.total_size_mb:.2f} MB\n" + else: + folders = folder_service.get_folders() + output = "Mailbox Statistics:\n" + total_messages = 0 + total_unread = 0 + + for folder in folders: + if folder.can_select: + output += f" {folder.name}: {folder.total_messages} total, {folder.unread_messages} unread\n" + total_messages += folder.total_messages + total_unread += folder.unread_messages + + output += f"\nOverall Total: {total_messages} messages, {total_unread} unread\n" + + return output + + except Exception as e: + return f"Error getting mailbox stats: {str(e)}" + + @mcp.tool() + async def get_unread_count(folder_name: str = None) -> str: + """Get unread message count + + Args: + folder_name: Specific folder name (optional, defaults to all folders) + """ + try: + unread_count = folder_service.get_unread_count(folder_name) + + if folder_name: + return f"Unread messages in '{folder_name}': {unread_count}" + else: + return f"Total unread messages: {unread_count}" + + except Exception as e: + return f"Error getting unread count: {str(e)}" \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/management_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/management_tools.py new file mode 100644 index 00000000..492b38ad --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/management_tools.py @@ -0,0 +1,482 @@ +from typing import List +from mcp.server.fastmcp import FastMCP +from ..services.draft_service import DraftService + + +def _reconstruct_email_message(email_obj) -> str: + """Reconstruct email message from EmailMessage object""" + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + from email.utils import formatdate + + # Create message + if email_obj.body_html: + msg = MIMEMultipart('alternative') + msg.attach(MIMEText(email_obj.body_text or '', 'plain', 'utf-8')) + msg.attach(MIMEText(email_obj.body_html, 'html', 'utf-8')) + else: + msg = MIMEText(email_obj.body_text or '', 'plain', 'utf-8') + + # Set headers + msg['Subject'] = email_obj.subject or '' + msg['From'] = email_obj.from_addr or '' + msg['To'] = email_obj.to_addr or '' + if email_obj.cc_addr: + msg['Cc'] = email_obj.cc_addr + if email_obj.bcc_addr: + msg['Bcc'] = email_obj.bcc_addr + if email_obj.message_id: + msg['Message-ID'] = email_obj.message_id + if email_obj.date: + msg['Date'] = email_obj.date + else: + msg['Date'] = formatdate(localtime=True) + + return msg.as_string() + +def register_management_tools(mcp: FastMCP, draft_service: DraftService, email_service): + """Register management and utility MCP tools""" + + @mcp.tool() + async def check_connection() -> str: + """Check email server connection status""" + try: + imap_ok, smtp_ok = email_service.check_connection() + + status = "Connection Status:\n" + status += f"IMAP: {'✓ Connected' if imap_ok else '✗ Failed'}\n" + status += f"SMTP: {'✓ Connected' if smtp_ok else '✗ Failed'}\n" + + if imap_ok and smtp_ok: + status += "\nAll connections are working properly" + elif imap_ok: + status += "\nWarning: SMTP connection failed - cannot send emails" + elif smtp_ok: + status += "\nWarning: IMAP connection failed - cannot receive emails" + else: + status += "\nError: Both connections failed - check configuration" + + return status + + except Exception as e: + return f"Error checking connection: {str(e)}" + + @mcp.tool() + async def get_email_headers(email_id: str) -> str: + """Get complete email headers for technical analysis + + Args: + email_id: Email ID to get headers for + """ + try: + email = email_service.imap_backend.fetch_email(email_id) + + if not email.raw_message: + return f"No raw message data available for email {email_id}" + + output = f"Email Headers for ID: {email_id}\n" + output += "=" * 50 + "\n" + + # Get all headers from raw message + for header, value in email.raw_message.items(): + output += f"{header}: {value}\n" + + return output + + except Exception as e: + return f"Error getting email headers: {str(e)}" + + @mcp.tool() + async def save_draft(subject: str, body: str, html_body: str = None, + to: str = None, cc: str = None, bcc: str = None) -> str: + """Save email draft + + Args: + subject: Email subject + body: Plain text body + html_body: HTML body content (optional) + to: Recipient email address(es) (optional) + cc: CC recipients (optional) + bcc: BCC recipients (optional) + """ + try: + draft_id = draft_service.save_draft( + subject=subject, + body=body, + html_body=html_body, + to=to, + cc=cc, + bcc=bcc + ) + + return f"Draft saved successfully with ID: {draft_id}" + + except Exception as e: + return f"Error saving draft: {str(e)}" + + @mcp.tool() + async def get_drafts(page: int = 1, page_size: int = 20) -> str: + """Get list of saved drafts + + Args: + page: Page number starting from 1 (default: 1) + page_size: Number of drafts per page (default: 20) + """ + try: + result = draft_service.get_drafts(page, page_size) + + if result['total_drafts'] == 0: + return "No drafts found" + + output = f"Drafts (Page {result['current_page']}/{result['total_pages']}):\n" + output += f"Total drafts: {result['total_drafts']}\n\n" + + for i, draft in enumerate(result['drafts'], 1): + output += f"{(result['current_page']-1)*result['page_size'] + i}. " + output += f"ID: {draft['draft_id']}\n" + output += f" Subject: {draft['subject']}\n" + output += f" To: {draft.get('to', 'Not set')}\n" + output += f" Updated: {draft['updated_at']}\n\n" + + return output + + except Exception as e: + return f"Error getting drafts: {str(e)}" + + @mcp.tool() + async def update_draft(draft_id: str, subject: str = None, body: str = None, + html_body: str = None, to: str = None, cc: str = None, bcc: str = None) -> str: + """Update existing draft + + Args: + draft_id: Draft ID to update + subject: Email subject (optional) + body: Plain text body (optional) + html_body: HTML body content (optional) + to: Recipient email address(es) (optional) + cc: CC recipients (optional) + bcc: BCC recipients (optional) + """ + try: + success = draft_service.update_draft( + draft_id=draft_id, + subject=subject, + body=body, + html_body=html_body, + to=to, + cc=cc, + bcc=bcc + ) + + if success: + return f"Draft {draft_id} updated successfully" + else: + return f"Failed to update draft {draft_id}" + + except Exception as e: + return f"Error updating draft: {str(e)}" + + @mcp.tool() + async def delete_draft(draft_id: str) -> str: + """Delete draft + + Args: + draft_id: Draft ID to delete + """ + try: + success = draft_service.delete_draft(draft_id) + + if success: + return f"Draft {draft_id} deleted successfully" + else: + return f"Failed to delete draft {draft_id}" + + except Exception as e: + return f"Error deleting draft: {str(e)}" + + @mcp.tool() + async def export_emails(folder: str = None, export_path: str = "emails_export.json", max_emails: int = None, export_all_folders: bool = False) -> str: + """Export emails to file for backup + + Args: + folder: Specific folder to export (mutually exclusive with export_all_folders) + export_path: Path where to save the export file + max_emails: Maximum number of emails to export (optional, exports all if not specified) + export_all_folders: Export from all folders instead of just one (default: False) + """ + try: + from ..backends.file_backend import FileBackend + from ..config import config_manager + from ..services.folder_service import FolderService + + # Initialize services + folder_service = FolderService(email_service.imap_backend) + + # Determine which folders to export from + if export_all_folders and folder: + return "Error: Cannot specify both 'folder' and 'export_all_folders=True'" + + folders_to_export = [] + if export_all_folders: + # Get all selectable folders + all_folders = folder_service.get_folders() + folders_to_export = [f.name for f in all_folders if f.can_select] + print(f"Exporting from all folders: {folders_to_export}") + else: + # Export from single folder + target_folder = folder or "INBOX" + folders_to_export = [target_folder] + + # Export from all specified folders + all_emails = [] + folder_stats = {} + + for folder_name in folders_to_export: + print(f"Processing folder: {folder_name}") + folder_emails = [] + page = 1 + page_size = 100 # Process in smaller batches + + while True: + try: + result = email_service.get_emails(folder_name, page=page, page_size=page_size) + + if not result.emails: + break + + folder_emails.extend(result.emails) + + # Check if we've reached the global maximum + if max_emails and len(all_emails) + len(folder_emails) >= max_emails: + remaining = max_emails - len(all_emails) + folder_emails = folder_emails[:remaining] + break + + # Check if we've got all emails from this folder + if page * page_size >= result.total_results: + break + + page += 1 + + except Exception as e: + print(f"Error reading from folder {folder_name}, page {page}: {str(e)}") + break + + # Add folder emails to total + all_emails.extend(folder_emails) + folder_stats[folder_name] = len(folder_emails) + + # Check global limit + if max_emails and len(all_emails) >= max_emails: + break + + if not all_emails: + folders_desc = "all folders" if export_all_folders else folders_to_export[0] + return f"No emails found to export from {folders_desc}" + + # Validate export path + workspace_config = config_manager.workspace_config + file_backend = FileBackend( + email_export_path=workspace_config.email_export_path if workspace_config else None, + attachment_download_path=workspace_config.attachment_download_path if workspace_config else None + ) + + export_name = "all_folders_export" if export_all_folders else f"{folders_to_export[0]}_export" + exported_file = file_backend.export_emails(all_emails, export_name, 'json') + + # Build result message + result_msg = f"Successfully exported {len(all_emails)} emails to {exported_file}\n" + + if export_all_folders: + result_msg += "Export breakdown by folder:\n" + for folder_name, count in folder_stats.items(): + result_msg += f" - {folder_name}: {count} emails\n" + + return result_msg.rstrip() + + except Exception as e: + return f"Error exporting emails: {str(e)}" + + @mcp.tool() + async def import_emails(import_path: str, target_folder: str = None, preserve_folders: bool = True) -> str: + """Import emails from backup file to IMAP server + + Args: + import_path: Path to import file (.json or .eml) or a directory + target_folder: Target folder for imported emails (if preserve_folders=False) + preserve_folders: Whether to preserve original folder structure (default: True) + """ + try: + from ..backends.file_backend import FileBackend + from ..config import config_manager + + # Validate import path + workspace_config = config_manager.workspace_config + file_backend = FileBackend( + email_export_path=workspace_config.email_export_path if workspace_config else None, + attachment_download_path=workspace_config.attachment_download_path if workspace_config else None + ) + + imported_emails = file_backend.import_emails(import_path) + + if not imported_emails: + return f"No emails found in import file {import_path}" + + # 按email_id从大到小排序,确保导入时保持原始顺序 + # 因为get_emails返回的是newest first,所以ID越大的邮件越新 + # 倒序导入可以保持原来的头部(最老)和尾部(最新)顺序 + try: + imported_emails.sort(key=lambda x: int(x.email_id) if x.email_id.isdigit() else 0, reverse=False) + print(f"Sorted {len(imported_emails)} emails by ID for proper import order") + except Exception as e: + print(f"Warning: Could not sort emails by ID: {str(e)}, importing in original order") + + # Import emails to IMAP server + success_count = 0 + failed_count = 0 + failed_reasons = [] + folder_stats = {} + + for email_obj in imported_emails: + try: + # Determine target folder + if preserve_folders and email_obj.folder: + import_folder = email_obj.folder + else: + import_folder = target_folder or "INBOX" + + # Ensure target folder exists and is accessible + folder_created = False + try: + email_service.imap_backend.select_folder(import_folder) + except Exception as e: + # If folder doesn't exist, try to create it + if preserve_folders and email_obj.folder and email_obj.folder not in ["INBOX", "SENT", "DRAFTS", "TRASH"]: + try: + from ..services.folder_service import FolderService + folder_service = FolderService(email_service.imap_backend) + + # Create folder and all necessary parent folders + success = folder_service.create_folder(import_folder) + if success: + folder_created = True + print(f"Created folder: {import_folder}") + # Re-select the newly created folder + email_service.imap_backend.select_folder(import_folder) + else: + raise Exception("Failed to create folder") + + except Exception as create_error: + # If can't create custom folder, fall back to INBOX + print(f"Warning: Cannot create folder '{import_folder}': {str(create_error)}") + print(f"Importing email {email_obj.email_id} to INBOX instead") + import_folder = "INBOX" + email_service.imap_backend.select_folder(import_folder) + else: + # For system folders or when preserve_folders=False, fail if can't access + failed_count += 1 + failed_reasons.append(f"Email {email_obj.email_id}: Cannot access folder '{import_folder}': {str(e)}") + continue + + # Convert EmailMessage back to raw email format if needed + if email_obj.raw_message: + # Use existing raw message + message_string = email_obj.raw_message.as_string() + else: + # Reconstruct email from EmailMessage data + message_string = _reconstruct_email_message(email_obj) + + # Import to IMAP server using APPEND command + success = email_service.imap_backend.append_message( + import_folder, + message_string, + flags='\\Seen' if email_obj.is_read else '' + ) + + if success: + success_count += 1 + # Track folder statistics + if import_folder not in folder_stats: + folder_stats[import_folder] = 0 + folder_stats[import_folder] += 1 + else: + failed_count += 1 + failed_reasons.append(f"Email {email_obj.email_id}: APPEND to '{import_folder}' failed") + + except Exception as e: + failed_count += 1 + failed_reasons.append(f"Email {email_obj.email_id}: {str(e)}") + + # Build result message + result_msg = f"Successfully imported {success_count}/{len(imported_emails)} emails" + + if preserve_folders and len(folder_stats) > 1: + result_msg += "\n\nImport breakdown by folder:" + for folder, count in folder_stats.items(): + result_msg += f"\n - {folder}: {count} emails" + elif folder_stats: + folder_name = list(folder_stats.keys())[0] + result_msg += f" to {folder_name}" + + if failed_count > 0: + result_msg += f"\n\n{failed_count} emails failed to import:" + for reason in failed_reasons[:5]: # Show first 5 failures + result_msg += f"\n - {reason}" + if len(failed_reasons) > 5: + result_msg += f"\n ... and {len(failed_reasons)-5} more" + + return result_msg + + except Exception as e: + return f"Error importing emails: {str(e)}" + + @mcp.tool() + async def download_attachment(email_id: str, attachment_filename: str) -> str: + """Download email attachment to configured download path + + Args: + email_id: Email ID containing the attachment + attachment_filename: Name of attachment to download + """ + try: + email = email_service.imap_backend.fetch_email(email_id) + + # Find the attachment + target_attachment = None + for attachment in email.attachments: + if attachment.filename == attachment_filename: + target_attachment = attachment + break + + if not target_attachment: + return f"Attachment '{attachment_filename}' not found in email {email_id}" + + # Extract attachment data from raw message + if not email.raw_message: + return f"No raw message data available for email {email_id}" + + attachment_data = None + for part in email.raw_message.walk(): + if part.get_filename() == attachment_filename: + attachment_data = part.get_payload(decode=True) + break + + if not attachment_data: + return f"Could not extract attachment data for '{attachment_filename}'" + + # Save attachment + from ..backends.file_backend import FileBackend + from ..config import config_manager + + workspace_config = config_manager.workspace_config + file_backend = FileBackend( + email_export_path=workspace_config.email_export_path if workspace_config else None, + attachment_download_path=workspace_config.attachment_download_path if workspace_config else None + ) + + saved_path = file_backend.save_attachment(attachment_data, attachment_filename) + + return f"Attachment '{attachment_filename}' saved to: {saved_path}" + + except Exception as e: + return f"Error downloading attachment: {str(e)}" \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/__init__.py new file mode 100644 index 00000000..20bb4042 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/__init__.py @@ -0,0 +1,33 @@ +from .exceptions import * +from .validators import * +from .email_parser import * + +__all__ = [ + # Exceptions + 'EmailMCPError', + 'ConnectionError', + 'AuthenticationError', + 'ConfigurationError', + 'ValidationError', + 'FolderError', + 'EmailNotFoundError', + 'AttachmentError', + 'SendEmailError', + + # Validators + 'validate_email_address', + 'validate_email_list', + 'validate_page_params', + 'validate_file_path', + 'validate_folder_name', + 'sanitize_subject', + 'validate_search_query', + + # Email parser + 'decode_email_header', + 'parse_email_addresses', + 'extract_attachments_info', + 'extract_email_body', + 'parse_raw_email', + 'format_email_summary' +] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/email_parser.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/email_parser.py new file mode 100644 index 00000000..8a79749e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/email_parser.py @@ -0,0 +1,309 @@ +import email +import email.message +import logging +from email.header import decode_header +from email.utils import parseaddr, formataddr +from typing import Dict, Any, List, Optional +from ..models.email import EmailMessage, EmailAttachment +from .exceptions import ValidationError + + +def decode_email_header(header_value: str) -> str: + """Decode email header properly handling encoding with improved Chinese support""" + if header_value is None: + return "" + + try: + decoded_parts = decode_header(header_value) + result = "" + for part, encoding in decoded_parts: + if isinstance(part, bytes): + if encoding: + try: + # Try the specified encoding first + result += part.decode(encoding) + except (UnicodeDecodeError, LookupError): + # Fallback to common Chinese encodings + for fallback_encoding in ['utf-8', 'gb2312', 'gbk', 'big5']: + try: + result += part.decode(fallback_encoding) + break + except (UnicodeDecodeError, LookupError): + continue + else: + # Final fallback with replacement characters + result += part.decode('utf-8', errors='replace') + else: + # No encoding specified, try UTF-8 first, then fallbacks + try: + result += part.decode('utf-8') + except UnicodeDecodeError: + for fallback_encoding in ['gb2312', 'gbk', 'big5', 'iso-8859-1']: + try: + result += part.decode(fallback_encoding) + break + except (UnicodeDecodeError, LookupError): + continue + else: + result += part.decode('utf-8', errors='replace') + else: + result += str(part) + return result.strip() + except Exception as e: + logging.warning(f"Failed to decode header: {str(e)}") + return str(header_value) + + +def parse_email_address_with_name(addr_string: str) -> tuple[str, str]: + """Parse email address with Chinese display name support + + Returns: + tuple[str, str]: (display_name, email_address) + """ + if not addr_string: + return "", "" + + try: + # First decode the header to handle Chinese names + decoded_addr = decode_email_header(addr_string) + + # Parse the address + display_name, email_addr = parseaddr(decoded_addr) + + # Clean up the display name and email + display_name = display_name.strip().strip('"').strip("'") + email_addr = email_addr.strip() + + return display_name, email_addr + except Exception as e: + logging.warning(f"Failed to parse email address '{addr_string}': {str(e)}") + return "", addr_string.strip() + + +def parse_email_addresses(addr_string: str) -> List[str]: + """Parse comma-separated email addresses with improved Chinese support""" + if not addr_string: + return [] + + addresses = [] + # Split by comma but be careful about commas in quoted names + parts = [] + current_part = "" + in_quotes = False + bracket_depth = 0 + + for char in addr_string: + if char == '"' and bracket_depth == 0: + in_quotes = not in_quotes + elif char == '<' and not in_quotes: + bracket_depth += 1 + elif char == '>' and not in_quotes: + bracket_depth -= 1 + elif char == ',' and not in_quotes and bracket_depth == 0: + parts.append(current_part.strip()) + current_part = "" + continue + current_part += char + + if current_part.strip(): + parts.append(current_part.strip()) + + for addr in parts: + if addr: + # Extract just the email part if in "Name " format + display_name, email_addr = parse_email_address_with_name(addr) + if email_addr: + addresses.append(email_addr) + + return addresses + + +def extract_attachments_info(msg: email.message.Message) -> List[EmailAttachment]: + """Extract attachment information from email message""" + attachments = [] + + if not msg.is_multipart(): + return attachments + + for part in msg.walk(): + disposition = part.get('Content-Disposition', '') + if 'attachment' in disposition: + filename = part.get_filename() + if filename: + # Decode filename if needed + filename = decode_email_header(filename) + + # Get content info + content_type = part.get_content_type() + # logging.debug(f"Filename: {filename}") + # logging.debug(f"Content type: {content_type}") + payload = part.get_payload(decode=True) + size = len(payload) if payload else 0 + + # logging.debug(f"Payload: {payload}") + # logging.debug(f"Size: {size}") + + attachment = EmailAttachment( + filename=filename, + content_type=content_type, + size=size, + content=payload # 保存附件的实际内容 + ) + attachments.append(attachment) + + return attachments + + +def detect_and_decode_content(payload: bytes, part: email.message.Message) -> str: + """Detect encoding and decode content with Chinese support""" + if not payload: + return "" + + # Get charset from content type + charset = part.get_content_charset() + + # Try the specified charset first + if charset: + try: + return payload.decode(charset) + except (UnicodeDecodeError, LookupError): + logging.debug(f"Failed to decode with specified charset: {charset}") + + # Try common encodings in order of preference + encodings_to_try = [ + 'utf-8', + 'gb2312', + 'gbk', + 'big5', + 'iso-8859-1', + 'windows-1252' + ] + + for encoding in encodings_to_try: + try: + return payload.decode(encoding) + except (UnicodeDecodeError, LookupError): + continue + + # Final fallback with replacement characters + return payload.decode('utf-8', errors='replace') + + +def extract_email_body(msg: email.message.Message) -> tuple[str, str]: + """Extract text and HTML body from email message with improved Chinese encoding support""" + body_text = "" + body_html = "" + + if msg.is_multipart(): + for part in msg.walk(): + content_type = part.get_content_type() + disposition = part.get('Content-Disposition', '') + + # Skip attachments + if 'attachment' in disposition: + continue + + payload = part.get_payload(decode=True) + if not payload: + continue + + try: + content = detect_and_decode_content(payload, part) + except Exception as e: + logging.warning(f"Failed to decode email content: {str(e)}") + continue + + if content_type == 'text/plain' and not body_text: + body_text = content + elif content_type == 'text/html' and not body_html: + body_html = content + else: + # Single part message + payload = msg.get_payload(decode=True) + if payload: + try: + content = detect_and_decode_content(payload, msg) + if msg.get_content_type() == 'text/html': + body_html = content + else: + body_text = content + except Exception as e: + logging.warning(f"Failed to decode single part message: {str(e)}") + + return body_text, body_html + + +def parse_raw_email(raw_email: bytes, email_id: str) -> EmailMessage: + """Parse raw email bytes into EmailMessage object with improved Chinese support""" + try: + msg = email.message_from_bytes(raw_email) + + # Extract headers with proper Chinese decoding + subject = decode_email_header(msg.get('Subject', '')) + + # Parse addresses with Chinese display name support + from_display_name, from_addr = parse_email_address_with_name(msg.get('From', '')) + to_display_name, to_addr = parse_email_address_with_name(msg.get('To', '')) + cc_addr = decode_email_header(msg.get('Cc', '')) or None + + # Store the original from address with display name for reply functionality + original_from = decode_email_header(msg.get('From', '')) + + date = msg.get('Date', '') + message_id = msg.get('Message-ID', '') + + # Extract body content with improved encoding detection + body_text, body_html = extract_email_body(msg) + + # Extract attachments + attachments = extract_attachments_info(msg) + + # Create EmailMessage with additional metadata + email_msg = EmailMessage( + email_id=email_id, + subject=subject, + from_addr=from_addr, + to_addr=to_addr, + cc_addr=cc_addr, + date=date, + body_text=body_text, + body_html=body_html, + attachments=attachments, + message_id=message_id + ) + + # Store the raw message for attachment extraction + email_msg.raw_message = msg + + # Add extra metadata for Chinese support + if hasattr(email_msg, '__dict__'): + email_msg.__dict__['from_display_name'] = from_display_name + email_msg.__dict__['to_display_name'] = to_display_name + email_msg.__dict__['original_from'] = original_from + + return email_msg + + except Exception as e: + logging.error(f"Failed to parse email {email_id}: {str(e)}") + raise ValidationError(f"Failed to parse email: {str(e)}") + + +def format_email_summary(email: EmailMessage, include_body_preview: bool = False) -> str: + """Format email for display summary""" + result = f"Subject: {email.subject}\n" + result += f"From: {email.from_addr}\n" + result += f"To: {email.to_addr}\n" + + if email.cc_addr: + result += f"CC: {email.cc_addr}\n" + + result += f"Date: {email.date}\n" + + if email.attachments: + result += f"Attachments: {len(email.attachments)} files\n" + + if include_body_preview and email.body_text: + preview = email.body_text[:200] + "..." if len(email.body_text) > 200 else email.body_text + result += f"Preview: {preview}\n" + + return result \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/encode_decode.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/encode_decode.py new file mode 100644 index 00000000..6d486cd7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/encode_decode.py @@ -0,0 +1,10 @@ +import imapclient + +def encode_to_imap_utf7(text): + """将文本编码为IMAP UTF-7格式""" + return imapclient.imap_utf7.encode(text).decode('ascii') + + +def decode_from_imap_utf7(encoded_text): + """将IMAP UTF-7格式解码为文本""" + return imapclient.imap_utf7.decode(encoded_text.encode('ascii')) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/exceptions.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/exceptions.py new file mode 100644 index 00000000..bf063278 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/exceptions.py @@ -0,0 +1,43 @@ +class EmailMCPError(Exception): + """Base exception for Email MCP operations""" + pass + + +class ConnectionError(EmailMCPError): + """Email server connection error""" + pass + + +class AuthenticationError(EmailMCPError): + """Email authentication error""" + pass + + +class ConfigurationError(EmailMCPError): + """Configuration error""" + pass + + +class ValidationError(EmailMCPError): + """Data validation error""" + pass + + +class FolderError(EmailMCPError): + """Email folder operation error""" + pass + + +class EmailNotFoundError(EmailMCPError): + """Email not found error""" + pass + + +class AttachmentError(EmailMCPError): + """Attachment operation error""" + pass + + +class SendEmailError(EmailMCPError): + """Email sending error""" + pass \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/validators.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/validators.py new file mode 100644 index 00000000..8460b18f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/validators.py @@ -0,0 +1,156 @@ +import re +from typing import Optional +from pathlib import Path +from .exceptions import ValidationError + + +def validate_email_address(email: str) -> bool: + """Validate email address format with international domain support""" + if not email or not isinstance(email, str): + return False + + # More flexible pattern that supports international domains + # Split into local and domain parts for separate validation + if '@' not in email: + return False + + local_part, domain_part = email.rsplit('@', 1) + + # Validate local part (before @) + # Allow ASCII characters and common symbols, but not international characters in local part + # This follows RFC 5321 more closely + if not local_part or len(local_part) > 64: + return False + + # Check for valid characters in local part (ASCII only for now) + # Allow letters, numbers, and common symbols + valid_local_chars = re.match(r'^[a-zA-Z0-9._%+-]+$', local_part) + if not valid_local_chars: + return False + + # Basic validation: no consecutive dots, no start/end with dot + if local_part.startswith('.') or local_part.endswith('.') or '..' in local_part: + return False + + # Validate domain part (after @) + if not domain_part or len(domain_part) > 255: + return False + + # Domain should have at least one dot and valid structure + if '.' not in domain_part: + return False + + # Split domain into parts + domain_parts = domain_part.split('.') + if len(domain_parts) < 2: + return False + + # Each domain part should not be empty and should have reasonable length + for part in domain_parts: + if not part or len(part) > 63: + return False + # Allow international characters in domain names (IDN support) + if not re.match(r'^[a-zA-Z0-9\u00a1-\uffff-]+$', part): + return False + + # TLD should be at least 2 characters (but allow international TLDs) + if len(domain_parts[-1]) < 2: + return False + + return True + + +def validate_email_list(email_list: str) -> tuple[bool, str]: + """Validate comma-separated email list""" + if not email_list: + return False, "Email list cannot be empty" + + emails = [email.strip() for email in email_list.split(',')] + invalid_emails = [] + + for email in emails: + if not validate_email_address(email): + invalid_emails.append(email) + + if invalid_emails: + return False, f"Invalid email addresses: {', '.join(invalid_emails)}" + + return True, "" + + +def validate_page_params(page: int, page_size: int, max_page_size: int = 50) -> tuple[int, int, str]: + """Validate and normalize pagination parameters""" + warning = "" + + if page < 1: + page = 1 + warning += "Page number adjusted to 1. " + + if page_size < 1: + page_size = 20 + warning += "Page size adjusted to 20. " + elif page_size > max_page_size: + page_size = max_page_size + warning += f"Page size limited to {max_page_size}. " + + return page, page_size, warning + + +def validate_file_path(file_path: str, must_exist: bool = True) -> tuple[bool, str]: + """Validate file path""" + if not file_path: + return False, "File path cannot be empty" + + try: + path = Path(file_path) + + if must_exist and not path.exists(): + return False, f"File does not exist: {file_path}" + + if must_exist and not path.is_file(): + return False, f"Path is not a file: {file_path}" + + return True, "" + + except Exception as e: + return False, f"Invalid file path: {str(e)}" + + +def validate_folder_name(folder_name: str) -> tuple[bool, str]: + """Validate email folder name""" + if not folder_name: + return False, "Folder name cannot be empty" + + # Basic validation - no path separators or special chars + invalid_chars = ['/', '\\', '..', '<', '>', ':', '"', '|', '?', '*'] + for char in invalid_chars: + if char in folder_name: + return False, f"Folder name contains invalid character: {char}" + + if len(folder_name) > 255: + return False, "Folder name too long (max 255 characters)" + + return True, "" + + +def sanitize_subject(subject: str) -> str: + """Sanitize email subject""" + if not subject: + return "" + + # Remove control characters and normalize whitespace + sanitized = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', subject) + sanitized = ' '.join(sanitized.split()) + + return sanitized[:998] # RFC limit for subject length + + +def validate_search_query(query: str) -> tuple[bool, str]: + """Validate search query""" + if not query or not query.strip(): + return False, "Search query cannot be empty" + + if len(query) > 1000: + return False, "Search query too long (max 1000 characters)" + + return True, "" \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/test_connection.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/test_connection.py new file mode 100644 index 00000000..cda4a408 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/test_connection.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +Email Connection Tester +Test IMAP and SMTP server connections +""" + +import argparse +import imaplib +import smtplib +import sys +from datetime import datetime + +class EmailConnectionTester: + """Email Connection Tester""" + + def __init__(self, email, password, imap_server, imap_port, smtp_server, smtp_port): + self.email = email + self.password = password + self.imap_server = imap_server + self.imap_port = imap_port + self.smtp_server = smtp_server + self.smtp_port = smtp_port + + def _connect_imap(self): + """Connect to IMAP server""" + print(f"\n📥 Connecting to IMAP server {self.imap_server}:{self.imap_port}...") + + try: + # Try normal connection first to check server capabilities + try: + print(" Trying SSL connection...") + imap = imaplib.IMAP4_SSL(self.imap_server, self.imap_port) + print(" ✅ SSL connection successful") + except: + print(" SSL connection failed, trying normal connection...") + imap = imaplib.IMAP4(self.imap_server, self.imap_port) + # Try STARTTLS + try: + print(" Trying to upgrade to TLS...") + imap.starttls() + print(" ✅ TLS upgrade successful") + except Exception as e: + print(f" ⚠️ STARTTLS failed: {e}, continuing with non-encrypted connection") + + # Login + print(f" Logging in to {self.email}...") + imap.login(self.email, self.password) + print(" ✅ IMAP login successful!") + + # Get email information + print("\n📊 Email information:") + # List all folders + status, folders = imap.list() + if status == 'OK': + print(f" 📁 Folder count: {len(folders)}") + print(" 📁 Folder list:") + max_display = 5 + for folder in folders[:max_display]: + print(f" - {folder.decode()}") + if len(folders) > max_display: + print(f" ... {len(folders)-max_display} more folders") + + # Select inbox + status, count = imap.select('INBOX') + if status == 'OK': + print(f" 📧 Inbox email count: {count[0].decode()}") + + # Close connection + imap.close() + imap.logout() + + return True + + except imaplib.IMAP4.error as e: + print(f" ❌ IMAP error: {e}") + return False + except Exception as e: + print(f" ❌ Connection failed: {type(e).__name__}: {e}") + return False + + def _connect_smtp(self): + """Connect to SMTP server""" + print(f"\n📤 Connecting to SMTP server {self.smtp_server}:{self.smtp_port}...") + + try: + # Try SSL connection first, then normal connection if SSL fails + try: + print(" Trying SSL connection...") + smtp = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) + print(" ✅ SSL connection successful") + except: + print(" SSL connection failed, trying normal connection...") + smtp = smtplib.SMTP(self.smtp_server, self.smtp_port) + smtp.set_debuglevel(1) # Set to 1 to see detailed SMTP conversation + + # 打招呼 + smtp.ehlo() + + # 检查服务器是否支持STARTTLS + if smtp.has_extn('STARTTLS'): + try: + print(" Detected STARTTLS support, trying to upgrade to TLS...") + smtp.starttls() + print(" Re-doing EHLO handshake...") + smtp.ehlo() + print(" ✅ TLS upgrade successful") + except Exception as e: + print(f" ⚠️ STARTTLS failed: {e}, continuing with non-encrypted connection") + else: + print(" ⚠️ Server does not support STARTTLS") + + # Login + print(f" Logging in to {self.email}...") + smtp.login(self.email, self.password) + print(" ✅ SMTP login successful!") + + # Get server information + print("\n📊 SMTP server features:") + if hasattr(smtp, 'esmtp_features'): + max_display = 5 + features = list(smtp.esmtp_features.keys())[:max_display] + for feature in features: + print(f" - {feature}") + if len(smtp.esmtp_features) > max_display: + print(f" ... {len(smtp.esmtp_features)-max_display} more features") + + # Close connection + smtp.quit() + + return True + + except smtplib.SMTPAuthenticationError: + print(" ❌ SMTP authentication failed: username or password error") + return False + except smtplib.SMTPException as e: + print(f" ❌ SMTP error: {e}") + return False + except Exception as e: + print(f" ❌ Connection failed: {type(e).__name__}: {e}") + return False + + def test_all(self): + """Test all connections""" + print("=" * 50) + print(f"📧 Email connection test") + print(f"⏰ Test time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 50) + + print(f"\n📋 Configuration:") + print(f" Email: {self.email}") + print(f" IMAP: {self.imap_server}:{self.imap_port}") + print(f" SMTP: {self.smtp_server}:{self.smtp_port}") + + # Test IMAP + imap_success = self._connect_imap() + + # Test SMTP + smtp_success = self._connect_smtp() + + # Summary + print("\n" + "=" * 50) + print("📊 Test results summary:") + print(f" IMAP: {'✅ Success' if imap_success else '❌ Failed'}") + print(f" SMTP: {'✅ Success' if smtp_success else '❌ Failed'}") + print("=" * 50) + + return imap_success and smtp_success + +def main(): + """Main function""" + parser = argparse.ArgumentParser( + description='Test email server connections', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Test Gmail + %(prog)s -e myemail@gmail.com -p mypassword -is imap.gmail.com -ip 993 -ss smtp.gmail.com -sp 587 + + # Test local Poste.io + %(prog)s -e user1@mcp.com -p password -is localhost -ip 1143 -ss localhost -sp 2525 + + # Simplified write (using short parameters) + %(prog)s -e test@test.com -p pass123 -is localhost -ip 143 -ss localhost -sp 25 + """ + ) + + # Add parameters + parser.add_argument('-e', '--email', required=True, help='Email address') + parser.add_argument('-p', '--password', required=True, help='Email password') + parser.add_argument('-is', '--imap-server', required=True, help='IMAP server address') + parser.add_argument('-ip', '--imap-port', type=int, required=True, help='IMAP port (143/993)') + parser.add_argument('-ss', '--smtp-server', required=True, help='SMTP server address') + parser.add_argument('-sp', '--smtp-port', type=int, required=True, help='SMTP port (25/465/587)') + + # Parse parameters + args = parser.parse_args() + + # Create tester and run + tester = EmailConnectionTester( + email=args.email, + password=args.password, + imap_server=args.imap_server, + imap_port=args.imap_port, + smtp_server=args.smtp_server, + smtp_port=args.smtp_port + ) + + # Run test + success = tester.test_all() + + # Return status code + sys.exit(0 if success else 1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/uv.lock new file mode 100644 index 00000000..69a7db37 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/uv.lock @@ -0,0 +1,1089 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +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" } +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" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +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/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" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { 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" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, +] + +[[package]] +name = "cyclopts" +version = "3.22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4'" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/d5/24c6c894f3833bc93d4944c2064309dfd633c0becf93e16fc79d76edd388/cyclopts-3.22.5.tar.gz", hash = "sha256:fa2450b9840abc41c6aa37af5eaeafc7a1264e08054e3a2fe39d49aa154f592a", size = 74890, upload-time = "2025-07-31T18:18:37.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/e5/a7b6db64f08cfe065e531ec6b508fa7dac704fab70d05adb5bc0c2c1d1b6/cyclopts-3.22.5-py3-none-any.whl", hash = "sha256:92efb4a094d9812718d7efe0bffa319a19cb661f230dbf24406c18cd8809fb82", size = 84994, upload-time = "2025-07-31T18:18:35.939Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "docutils" +version = "0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, +] + +[[package]] +name = "emails-mcp" +version = "0.1.11" +source = { editable = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "email-validator" }, + { name = "fastmcp" }, + { name = "imapclient" }, + { name = "mcp", extra = ["cli"] }, + { name = "psycopg2-binary" }, + { name = "requests" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "email-validator", specifier = ">=2.1.1" }, + { name = "fastmcp", specifier = ">=2.10.5" }, + { name = "imapclient", specifier = ">=3.0.1" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.11.0" }, + { name = "psycopg2-binary", specifier = ">=2.9" }, + { name = "requests", specifier = ">=2.32.4" }, + { name = "typing-extensions", specifier = ">=4.9.0" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "fastmcp" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "openapi-core" }, + { name = "openapi-pydantic" }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/89/d100073d15cdfa5fa029107b44ef55916b04ed6010ff2b0f7bed92a35ed9/fastmcp-2.11.1.tar.gz", hash = "sha256:2b5af21b093d4926fef17a9a162d5729a2fcb46f3b195699762fa01f61ac3c60", size = 2672724, upload-time = "2025-08-04T15:39:29.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/9f/f3703867a8be93f2a139f6664fa7ff46c5c844e28998ce288f7b919ed197/fastmcp-2.11.1-py3-none-any.whl", hash = "sha256:9f0b6a3f61dcf6f688a0a24b8b507be24bfae051a00b7d590c01395d63da8c00", size = 256573, upload-time = "2025-08-04T15:39:27.594Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +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/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 = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "imapclient" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/63/0eea51c9c263c18021cdc5866def55c98393f3bd74bbb8e3053e36f0f81a/IMAPClient-3.0.1.zip", hash = "sha256:78e6d62fbfbbe233e1f0e0e993160fd665eb1fd35973acddc61c15719b22bc02", size = 244222, upload-time = "2023-12-02T08:24:15.344Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/8a/d1364c1c6d8f53ea390e8f1c6da220a4f9ee478ac8a473ae0669a2fb6f51/IMAPClient-3.0.1-py2.py3-none-any.whl", hash = "sha256:d77d77caa4123e0233b5cf2b9c54a078522e63270b88d3f48653a28637fd8828", size = 182490, upload-time = "2023-12-02T08:24:11.854Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.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" } +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" }, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/1f56571ed82fb324f293661690635cf42c41deb8a70a6c9e6edc3e9bb3c8/lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c", size = 44736, upload-time = "2025-04-16T16:53:48.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/24/dae4759469e9cd318fef145f7cfac7318261b47b23a4701aa477b0c3b42c/lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b", size = 28142, upload-time = "2025-04-16T16:53:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/645a881f5f27952a02f24584d96f9f326748be06ded2cee25f8f8d1cd196/lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3", size = 28380, upload-time = "2025-04-16T16:53:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0f/6e004f928f7ff5abae2b8e1f68835a3870252f886e006267702e1efc5c7b/lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd", size = 28149, upload-time = "2025-04-16T16:53:40.135Z" }, + { url = "https://files.pythonhosted.org/packages/63/cb/b8363110e32cc1fd82dc91296315f775d37a39df1c1cfa976ec1803dac89/lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7", size = 28389, upload-time = "2025-04-16T16:53:43.612Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/68c50fcfd81e11480cd8ee7f654c9bd790a9053b9a0efe9983d46106f6a9/lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3", size = 28777, upload-time = "2025-04-16T16:53:41.371Z" }, + { url = "https://files.pythonhosted.org/packages/39/d0/7e967689e24de8ea6368ec33295f9abc94b9f3f0cd4571bfe148dc432190/lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8", size = 29598, upload-time = "2025-04-16T16:53:42.513Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1e/fb441c07b6662ec1fc92b249225ba6e6e5221b05623cb0131d082f782edc/lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b", size = 16635, upload-time = "2025-04-16T16:53:47.198Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mcp" +version = "1.12.3" +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/4d/19/9955e2df5384ff5dd25d38f8e88aaf89d2d3d9d39f27e7383eaf0b293836/mcp-1.12.3.tar.gz", hash = "sha256:ab2e05f5e5c13e1dc90a4a9ef23ac500a6121362a564447855ef0ab643a99fed", size = 427203, upload-time = "2025-07-31T18:36:36.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8b/0be74e3308a486f1d127f3f6767de5f9f76454c9b4183210c61cc50999b6/mcp-1.12.3-py3-none-any.whl", hash = "sha256:5483345bf39033b858920a5b6348a303acacf45b23936972160ff152107b850e", size = 158810, upload-time = "2025-07-31T18:36:34.915Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.7.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" } +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" }, +] + +[[package]] +name = "openapi-core" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "more-itertools" }, + { name = "openapi-schema-validator" }, + { name = "openapi-spec-validator" }, + { name = "parse" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, +] + +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, +] + +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +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" } +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" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { 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" } +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" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" } + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { 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" } +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" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" }, + { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" }, + { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" }, + { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" }, + { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" }, + { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" }, + { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" }, + { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" }, + { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload-time = "2025-07-01T15:55:20.399Z" }, + { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload-time = "2025-07-01T15:55:21.729Z" }, + { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload-time = "2025-07-01T15:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload-time = "2025-07-01T15:55:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload-time = "2025-07-01T15:55:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload-time = "2025-07-01T15:55:27.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload-time = "2025-07-01T15:55:29.057Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload-time = "2025-07-01T15:55:30.719Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload-time = "2025-07-01T15:55:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload-time = "2025-07-01T15:55:33.312Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload-time = "2025-07-01T15:55:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload-time = "2025-07-01T15:55:36.202Z" }, + { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload-time = "2025-07-01T15:55:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload-time = "2025-07-01T15:55:42.015Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload-time = "2025-07-01T15:55:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload-time = "2025-07-01T15:55:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload-time = "2025-07-01T15:55:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload-time = "2025-07-01T15:55:48.412Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload-time = "2025-07-01T15:55:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload-time = "2025-07-01T15:55:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" }, + { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +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" } +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" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +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/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.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +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 = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.github/workflows/publish.yml new file mode 100644 index 00000000..acacdc5c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.github/workflows/publish.yml @@ -0,0 +1,34 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build-n-publish: + name: Build and publish to PyPI + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/project/excel-mcp-server + permissions: + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Install hatch dependencies + run: | + python -m pip install --upgrade pip + pip install hatch + + - name: Build package + run: hatch build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.gitignore new file mode 100644 index 00000000..67bec94c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.gitignore @@ -0,0 +1,6 @@ +.venv/ +dist/ +excel_files/ +__pycache__/ +.notes/ +*.log \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.mcpbignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.mcpbignore new file mode 100644 index 00000000..3d5b7bb6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.mcpbignore @@ -0,0 +1,57 @@ +# Exclude everything except manifest and icon (uvx pattern) +# The uvx command downloads the package from PyPI at runtime + +# Python source files +*.py +*.pyc +*.pyo +__pycache__/ + +# Package files +*.toml +*.lock +*.txt +*.cfg +*.ini + +# Source directories +src/ +tests/ +docs/ + +# Assets (icon is copied to root) +assets/ + +# Documentation +*.md +!README.md + +# Build artifacts +*.egg-info/ +dist/ +build/ +.eggs/ + +# Environment +.env* +*.local +.venv/ +venv/ + +# IDE/Editor +.vscode/ +.idea/ +*.swp +*.swo + +# Git +.git/ +.gitignore +.gitattributes + +# CI/CD +.github/ + +# Misc +.DS_Store +Thumbs.db diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.python-version new file mode 100644 index 00000000..7c7a975f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.python-version @@ -0,0 +1 @@ +3.10 \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/LICENSE new file mode 100644 index 00000000..1f121f23 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Haris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/README.md new file mode 100644 index 00000000..eede6e4d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/README.md @@ -0,0 +1,114 @@ +

+ Excel MCP Server Logo +

+ +[![PyPI version](https://img.shields.io/pypi/v/excel-mcp-server.svg)](https://pypi.org/project/excel-mcp-server/) +[![Total Downloads](https://static.pepy.tech/badge/excel-mcp-server)](https://pepy.tech/project/excel-mcp-server) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![smithery badge](https://smithery.ai/badge/@haris-musa/excel-mcp-server)](https://smithery.ai/server/@haris-musa/excel-mcp-server) +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=excel-mcp-server&config=eyJjb21tYW5kIjoidXZ4IGV4Y2VsLW1jcC1zZXJ2ZXIgc3RkaW8ifQ%3D%3D) + +A Model Context Protocol (MCP) server that lets you manipulate Excel files without needing Microsoft Excel installed. Create, read, and modify Excel workbooks with your AI agent. + +## Features + +- 📊 **Excel Operations**: Create, read, update workbooks and worksheets +- 📈 **Data Manipulation**: Formulas, formatting, charts, pivot tables, and Excel tables +- 🔍 **Data Validation**: Built-in validation for ranges, formulas, and data integrity +- 🎨 **Formatting**: Font styling, colors, borders, alignment, and conditional formatting +- 📋 **Table Operations**: Create and manage Excel tables with custom styling +- 📊 **Chart Creation**: Generate various chart types (line, bar, pie, scatter, etc.) +- 🔄 **Pivot Tables**: Create dynamic pivot tables for data analysis +- 🔧 **Sheet Management**: Copy, rename, delete worksheets with ease +- 🔌 **Triple transport support**: stdio, SSE (deprecated), and streamable HTTP +- 🌐 **Remote & Local**: Works both locally and as a remote service + +## Usage + +The server supports three transport methods: + +### 1. Stdio Transport (for local use) + +```bash +uvx excel-mcp-server stdio +``` + +```json +{ + "mcpServers": { + "excel": { + "command": "uvx", + "args": ["excel-mcp-server", "stdio"] + } + } +} +``` + +### 2. SSE Transport (Server-Sent Events - Deprecated) + +```bash +uvx excel-mcp-server sse +``` + +**SSE transport connection**: +```json +{ + "mcpServers": { + "excel": { + "url": "http://localhost:8000/sse", + } + } +} +``` + +### 3. Streamable HTTP Transport (Recommended for remote connections) + +```bash +uvx excel-mcp-server streamable-http +``` + +**Streamable HTTP transport connection**: +```json +{ + "mcpServers": { + "excel": { + "url": "http://localhost:8000/mcp", + } + } +} +``` + +## Environment Variables & File Path Handling + +### SSE and Streamable HTTP Transports + +When running the server with the **SSE or Streamable HTTP protocols**, you **must set the `EXCEL_FILES_PATH` environment variable on the server side**. This variable tells the server where to read and write Excel files. +- If not set, it defaults to `./excel_files`. + +You can also set the `FASTMCP_PORT` environment variable to control the port the server listens on (default is `8017` if not set). +- Example (Windows PowerShell): + ```powershell + $env:EXCEL_FILES_PATH="E:\MyExcelFiles" + $env:FASTMCP_PORT="8007" + uvx excel-mcp-server streamable-http + ``` +- Example (Linux/macOS): + ```bash + EXCEL_FILES_PATH=/path/to/excel_files FASTMCP_PORT=8007 uvx excel-mcp-server streamable-http + ``` + +### Stdio Transport + +When using the **stdio protocol**, the file path is provided with each tool call, so you do **not** need to set `EXCEL_FILES_PATH` on the server. The server will use the path sent by the client for each operation. + +## Available Tools + +The server provides a comprehensive set of Excel manipulation tools. See [TOOLS.md](TOOLS.md) for complete documentation of all available tools. + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=haris-musa/excel-mcp-server&type=Date)](https://www.star-history.com/#haris-musa/excel-mcp-server&Date) + +## License + +MIT License - see [LICENSE](LICENSE) for details. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/TOOLS.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/TOOLS.md new file mode 100644 index 00000000..2d8f71d1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/TOOLS.md @@ -0,0 +1,473 @@ +# Excel MCP Server Tools + +This document provides detailed information about all available tools in the Excel MCP server. + +## Workbook Operations + +### create_workbook + +Creates a new Excel workbook. + +```python +create_workbook(filepath: str) -> str +``` + +- `filepath`: Path where to create workbook +- Returns: Success message with created file path + +### create_worksheet + +Creates a new worksheet in an existing workbook. + +```python +create_worksheet(filepath: str, sheet_name: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Name for the new worksheet +- Returns: Success message + +### get_workbook_metadata + +Get metadata about workbook including sheets and ranges. + +```python +get_workbook_metadata(filepath: str, include_ranges: bool = False) -> str +``` + +- `filepath`: Path to Excel file +- `include_ranges`: Whether to include range information +- Returns: String representation of workbook metadata + +## Data Operations + +### write_data_to_excel + +Write data to Excel worksheet. + +```python +write_data_to_excel( + filepath: str, + sheet_name: str, + data: List[Dict], + start_cell: str = "A1" +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `data`: List of dictionaries containing data to write +- `start_cell`: Starting cell (default: "A1") +- Returns: Success message + +### read_data_from_excel + +Read data from Excel worksheet. + +```python +read_data_from_excel( + filepath: str, + sheet_name: str, + start_cell: str = "A1", + end_cell: str = None, + preview_only: bool = False +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Source worksheet name +- `start_cell`: Starting cell (default: "A1") +- `end_cell`: Optional ending cell +- `preview_only`: Whether to return only a preview +- Returns: String representation of data + +## Formatting Operations + +### format_range + +Apply formatting to a range of cells. + +```python +format_range( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: str = None, + bold: bool = False, + italic: bool = False, + underline: bool = False, + font_size: int = None, + font_color: str = None, + bg_color: str = None, + border_style: str = None, + border_color: str = None, + number_format: str = None, + alignment: str = None, + wrap_text: bool = False, + merge_cells: bool = False, + protection: Dict[str, Any] = None, + conditional_format: Dict[str, Any] = None +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_cell`: Starting cell of range +- `end_cell`: Optional ending cell of range +- Various formatting options (see parameters) +- Returns: Success message + +### merge_cells + +Merge a range of cells. + +```python +merge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_cell`: Starting cell of range +- `end_cell`: Ending cell of range +- Returns: Success message + +### unmerge_cells + +Unmerge a previously merged range of cells. + +```python +unmerge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_cell`: Starting cell of range +- `end_cell`: Ending cell of range +- Returns: Success message + +### get_merged_cells + +Get merged cells in a worksheet. + +```python +get_merged_cells(filepath: str, sheet_name: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- Returns: String representation of merged cells + + +## Formula Operations + +### apply_formula + +Apply Excel formula to cell. + +```python +apply_formula(filepath: str, sheet_name: str, cell: str, formula: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `cell`: Target cell reference +- `formula`: Excel formula to apply +- Returns: Success message + +### validate_formula_syntax + +Validate Excel formula syntax without applying it. + +```python +validate_formula_syntax(filepath: str, sheet_name: str, cell: str, formula: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `cell`: Target cell reference +- `formula`: Excel formula to validate +- Returns: Validation result message + +## Chart Operations + +### create_chart + +Create chart in worksheet. + +```python +create_chart( + filepath: str, + sheet_name: str, + data_range: str, + chart_type: str, + target_cell: str, + title: str = "", + x_axis: str = "", + y_axis: str = "" +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `data_range`: Range containing chart data +- `chart_type`: Type of chart (line, bar, pie, scatter, area) +- `target_cell`: Cell where to place chart +- `title`: Optional chart title +- `x_axis`: Optional X-axis label +- `y_axis`: Optional Y-axis label +- Returns: Success message + +## Pivot Table Operations + +### create_pivot_table + +Create pivot table in worksheet. + +```python +create_pivot_table( + filepath: str, + sheet_name: str, + data_range: str, + target_cell: str, + rows: List[str], + values: List[str], + columns: List[str] = None, + agg_func: str = "mean" +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `data_range`: Range containing source data +- `target_cell`: Cell where to place pivot table +- `rows`: Fields for row labels +- `values`: Fields for values +- `columns`: Optional fields for column labels +- `agg_func`: Aggregation function (sum, count, average, max, min) +- Returns: Success message + +## Table Operations + +### create_table + +Creates a native Excel table from a specified range of data. + +```python +create_table( + filepath: str, + sheet_name: str, + data_range: str, + table_name: str = None, + table_style: str = "TableStyleMedium9" +) -> str +``` + +- `filepath`: Path to the Excel file. +- `sheet_name`: Name of the worksheet. +- `data_range`: The cell range for the table (e.g., "A1:D5"). +- `table_name`: Optional unique name for the table. +- `table_style`: Optional visual style for the table. +- Returns: Success message. + +## Worksheet Operations + +### copy_worksheet + +Copy worksheet within workbook. + +```python +copy_worksheet(filepath: str, source_sheet: str, target_sheet: str) -> str +``` + +- `filepath`: Path to Excel file +- `source_sheet`: Name of sheet to copy +- `target_sheet`: Name for new sheet +- Returns: Success message + +### delete_worksheet + +Delete worksheet from workbook. + +```python +delete_worksheet(filepath: str, sheet_name: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Name of sheet to delete +- Returns: Success message + +### rename_worksheet + +Rename worksheet in workbook. + +```python +rename_worksheet(filepath: str, old_name: str, new_name: str) -> str +``` + +- `filepath`: Path to Excel file +- `old_name`: Current sheet name +- `new_name`: New sheet name +- Returns: Success message + +## Range Operations + +### copy_range + +Copy a range of cells to another location. + +```python +copy_range( + filepath: str, + sheet_name: str, + source_start: str, + source_end: str, + target_start: str, + target_sheet: str = None +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Source worksheet name +- `source_start`: Starting cell of source range +- `source_end`: Ending cell of source range +- `target_start`: Starting cell for paste +- `target_sheet`: Optional target worksheet name +- Returns: Success message + +### delete_range + +Delete a range of cells and shift remaining cells. + +```python +delete_range( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: str, + shift_direction: str = "up" +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_cell`: Starting cell of range +- `end_cell`: Ending cell of range +- `shift_direction`: Direction to shift cells ("up" or "left") +- Returns: Success message + +### validate_excel_range + +Validate if a range exists and is properly formatted. + +```python +validate_excel_range( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: str = None +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_cell`: Starting cell of range +- `end_cell`: Optional ending cell of range +- Returns: Validation result message + +### get_data_validation_info + +Get data validation rules and metadata for a worksheet. + +```python +get_data_validation_info(filepath: str, sheet_name: str) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- Returns: JSON string containing all data validation rules with metadata including: + - Validation type (list, whole, decimal, date, time, textLength) + - Operator (between, notBetween, equal, greaterThan, lessThan, etc.) + - Allowed values for list validations (resolved from ranges) + - Formula constraints for numeric/date validations + - Cell ranges where validation applies + - Prompt and error messages + +**Note**: The `read_data_from_excel` tool automatically includes validation metadata for individual cells when available. + +## Row and Column Operations + +### insert_rows + +Insert one or more rows starting at the specified row. + +```python +insert_rows( + filepath: str, + sheet_name: str, + start_row: int, + count: int = 1 +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_row`: Row number where to start inserting (1-based) +- `count`: Number of rows to insert (default: 1) +- Returns: Success message + +### insert_columns + +Insert one or more columns starting at the specified column. + +```python +insert_columns( + filepath: str, + sheet_name: str, + start_col: int, + count: int = 1 +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_col`: Column number where to start inserting (1-based) +- `count`: Number of columns to insert (default: 1) +- Returns: Success message + +### delete_sheet_rows + +Delete one or more rows starting at the specified row. + +```python +delete_sheet_rows( + filepath: str, + sheet_name: str, + start_row: int, + count: int = 1 +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_row`: Row number where to start deleting (1-based) +- `count`: Number of rows to delete (default: 1) +- Returns: Success message + +### delete_sheet_columns + +Delete one or more columns starting at the specified column. + +```python +delete_sheet_columns( + filepath: str, + sheet_name: str, + start_col: int, + count: int = 1 +) -> str +``` + +- `filepath`: Path to Excel file +- `sheet_name`: Target worksheet name +- `start_col`: Column number where to start deleting (1-based) +- `count`: Number of columns to delete (default: 1) +- Returns: Success message diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.png new file mode 100644 index 00000000..dc8a3934 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.svg b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.svg new file mode 100644 index 00000000..1e85b4f6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.svg @@ -0,0 +1,8 @@ + + + + Excel + MCP + Server + + \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/CNAME b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/CNAME new file mode 100644 index 00000000..9c8ae693 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/CNAME @@ -0,0 +1 @@ +excelmcpserver.com \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/index.html b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/index.html new file mode 100644 index 00000000..38cc571a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/index.html @@ -0,0 +1,182 @@ + + + + + + Excel MCP Server + + + +
+
+

excel-mcp-server

+ + + View on GitHub + Loading... + +
+ +
+
+

Excel MCP Server

+

A Model Context Protocol (MCP) server that lets you manipulate Excel files without needing Microsoft Excel installed. Create, read, and modify Excel workbooks with your AI agent.

+
+ +
+

Features

+
    +
  • 📊 Create and modify Excel workbooks
  • +
  • 📝 Read and write data
  • +
  • 🎨 Apply formatting and styles
  • +
  • 📈 Create charts and visualizations
  • +
  • 📊 Generate pivot tables
  • +
  • 🔄 Manage worksheets and ranges
  • +
  • 🔌 Triple transport support: stdio, streamable HTTP, and SSE
  • +
+
+ +
+

Available Tools

+

A comprehensive set of tools to interact with your Excel files.

+
    +
  • Workbook Operations: Create workbooks and worksheets, and get metadata.
  • +
  • Data Operations: Read and write data to worksheets.
  • +
  • Formatting Operations: Apply rich formatting to cell ranges.
  • +
  • Formula Operations: Apply and validate Excel formulas.
  • +
  • Chart Operations: Create various types of charts.
  • +
  • Pivot Table Operations: Generate pivot tables from your data.
  • +
  • Table Operations: Create native Excel tables.
  • +
+
+
+ +
+

© 2025 excelmcpserver.com - An open source project by haris-musa.

+
+
+ + + \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/excel-mcp-server-0.1.7.mcpb b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/excel-mcp-server-0.1.7.mcpb new file mode 100644 index 00000000..1f70d346 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/excel-mcp-server-0.1.7.mcpb differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/icon.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/icon.png new file mode 100644 index 00000000..dc8a3934 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/icon.png differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/manifest.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/manifest.json new file mode 100644 index 00000000..815d5f66 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/manifest.json @@ -0,0 +1,53 @@ +{ + "manifest_version": "0.3", + "name": "excel-mcp-server", + "version": "0.1.7", + "description": "A Model Context Protocol server for Excel file manipulation", + "author": { + "name": "haris", + "url": "https://github.com/haris-musa" + }, + "repository": { + "type": "git", + "url": "https://github.com/haris-musa/excel-mcp-server" + }, + "homepage": "https://github.com/haris-musa/excel-mcp-server", + "documentation": "https://github.com/haris-musa/excel-mcp-server#readme", + "support": "https://github.com/haris-musa/excel-mcp-server/issues", + "icon": "icon.png", + "server": { + "type": "python", + "entry_point": "src/excel_mcp/__main__.py", + "mcp_config": { + "command": "uvx", + "args": ["excel-mcp-server", "stdio"] + } + }, + "tools": [ + {"name": "create_workbook", "description": "Create a new Excel workbook"}, + {"name": "create_worksheet", "description": "Create a new worksheet in a workbook"}, + {"name": "get_workbook_metadata", "description": "Get workbook metadata and structure"}, + {"name": "write_data_to_excel", "description": "Write data to Excel cells"}, + {"name": "read_data_from_excel", "description": "Read data from Excel range"}, + {"name": "format_range", "description": "Apply formatting to cell range"}, + {"name": "merge_cells", "description": "Merge cells in a range"}, + {"name": "unmerge_cells", "description": "Unmerge previously merged cells"}, + {"name": "get_merged_cells", "description": "Get list of merged cell ranges"}, + {"name": "apply_formula", "description": "Apply Excel formula to cell"}, + {"name": "validate_formula_syntax", "description": "Validate Excel formula syntax"}, + {"name": "create_chart", "description": "Create chart (line, bar, pie, scatter, etc.)"}, + {"name": "create_pivot_table", "description": "Create pivot table from data"}, + {"name": "create_table", "description": "Create Excel table with styling"}, + {"name": "copy_worksheet", "description": "Copy worksheet within workbook"}, + {"name": "delete_worksheet", "description": "Delete worksheet from workbook"}, + {"name": "rename_worksheet", "description": "Rename a worksheet"}, + {"name": "copy_range", "description": "Copy cell range to another location"}, + {"name": "delete_range", "description": "Delete cell range contents"}, + {"name": "validate_excel_range", "description": "Validate Excel range format"}, + {"name": "get_data_validation_info", "description": "Get data validation rules for cell"}, + {"name": "insert_rows", "description": "Insert rows into worksheet"}, + {"name": "insert_columns", "description": "Insert columns into worksheet"}, + {"name": "delete_sheet_rows", "description": "Delete rows from worksheet"}, + {"name": "delete_sheet_columns", "description": "Delete columns from worksheet"} + ] +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/pyproject.toml new file mode 100644 index 00000000..6e1e3c77 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "excel-mcp-server" +version = "0.1.7" +description = "Excel MCP Server for manipulating Excel files" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "mcp[cli]>=1.10.1", + "fastmcp>=2.0.0,<3.0.0", + "openpyxl>=3.1.5", + "typer>=0.16.0" +] +[[project.authors]] +name = "haris" +email = "haris.musa@outlook.com" + +[project.scripts] +excel-mcp-server = "excel_mcp.__main__:app" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/excel_mcp"] + +[tool.hatch.build] +packages = ["src/excel_mcp"] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/__main__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/__main__.py new file mode 100644 index 00000000..950187fe --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/__main__.py @@ -0,0 +1,50 @@ +import typer + +from .server import run_sse, run_stdio, run_streamable_http + +app = typer.Typer(help="Excel MCP Server") + +@app.command() +def sse(): + """Start Excel MCP Server in SSE mode""" + try: + run_sse() + except KeyboardInterrupt: + print("\nShutting down server...") + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() + finally: + print("Service stopped.") + +@app.command() +def streamable_http(): + """Start Excel MCP Server in streamable HTTP mode""" + try: + run_streamable_http() + except KeyboardInterrupt: + print("\nShutting down server...") + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() + finally: + print("Service stopped.") + +@app.command() +def stdio(): + """Start Excel MCP Server in stdio mode""" + try: + run_stdio() + except KeyboardInterrupt: + print("\nShutting down server...") + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() + finally: + print("Service stopped.") + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/calculations.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/calculations.py new file mode 100644 index 00000000..a46e12b0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/calculations.py @@ -0,0 +1,60 @@ +from typing import Any +import logging + +from .workbook import get_or_create_workbook +from .cell_utils import validate_cell_reference +from .exceptions import ValidationError, CalculationError +from .validation import validate_formula + +logger = logging.getLogger(__name__) + +def apply_formula( + filepath: str, + sheet_name: str, + cell: str, + formula: str +) -> dict[str, Any]: + """Apply any Excel formula to a cell.""" + try: + if not validate_cell_reference(cell): + raise ValidationError(f"Invalid cell reference: {cell}") + + wb = get_or_create_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise ValidationError(f"Sheet '{sheet_name}' not found") + + sheet = wb[sheet_name] + + # Ensure formula starts with = + if not formula.startswith('='): + formula = f'={formula}' + + # Validate formula syntax + is_valid, message = validate_formula(formula) + if not is_valid: + raise CalculationError(f"Invalid formula syntax: {message}") + + try: + # Apply formula to the cell + cell_obj = sheet[cell] + cell_obj.value = formula + except Exception as e: + raise CalculationError(f"Failed to apply formula to cell: {str(e)}") + + try: + wb.save(filepath) + except Exception as e: + raise CalculationError(f"Failed to save workbook after applying formula: {str(e)}") + + return { + "message": f"Applied formula '{formula}' to cell {cell}", + "cell": cell, + "formula": formula + } + + except (ValidationError, CalculationError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to apply formula: {e}") + raise CalculationError(str(e)) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_utils.py new file mode 100644 index 00000000..32fe8226 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_utils.py @@ -0,0 +1,54 @@ +import re + +from openpyxl.utils import column_index_from_string + +def parse_cell_range( + cell_ref: str, + end_ref: str | None = None +) -> tuple[int, int, int | None, int | None]: + """Parse Excel cell reference into row and column indices.""" + if end_ref: + start_cell = cell_ref + end_cell = end_ref + else: + start_cell = cell_ref + end_cell = None + + match = re.match(r"([A-Z]+)([0-9]+)", start_cell.upper()) + if not match: + raise ValueError(f"Invalid cell reference: {start_cell}") + col_str, row_str = match.groups() + start_row = int(row_str) + start_col = column_index_from_string(col_str) + + if end_cell: + match = re.match(r"([A-Z]+)([0-9]+)", end_cell.upper()) + if not match: + raise ValueError(f"Invalid cell reference: {end_cell}") + col_str, row_str = match.groups() + end_row = int(row_str) + end_col = column_index_from_string(col_str) + else: + end_row = None + end_col = None + + return start_row, start_col, end_row, end_col + +def validate_cell_reference(cell_ref: str) -> bool: + """Validate Excel cell reference format (e.g., 'A1', 'BC123')""" + if not cell_ref: + return False + + # Split into column and row parts + col = row = "" + for c in cell_ref: + if c.isalpha(): + if row: # Letters after numbers not allowed + return False + col += c + elif c.isdigit(): + row += c + else: + return False + + return bool(col and row) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_validation.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_validation.py new file mode 100644 index 00000000..ef14f145 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_validation.py @@ -0,0 +1,179 @@ +import logging +from typing import Any, Dict, List, Optional + +from openpyxl.worksheet.worksheet import Worksheet +from openpyxl.utils.cell import coordinate_from_string, column_index_from_string + +logger = logging.getLogger(__name__) + +def get_data_validation_for_cell(worksheet: Worksheet, cell_address: str) -> Optional[Dict[str, Any]]: + """Get data validation metadata for a specific cell. + + Args: + worksheet: The openpyxl worksheet object + cell_address: Cell address like 'A1', 'B2', etc. + + Returns: + Dictionary with validation metadata or None if no validation exists + """ + try: + # Convert cell address to row/col coordinates + col_letter, row = coordinate_from_string(cell_address) + col_idx = column_index_from_string(col_letter) + + # Check each data validation rule in the worksheet + for dv in worksheet.data_validations.dataValidation: + # Check if this cell is covered by the validation rule + if _cell_in_validation_range(row, col_idx, dv): + return _extract_validation_metadata(dv, cell_address, worksheet) + + return None + + except Exception as e: + logger.warning(f"Failed to get validation for cell {cell_address}: {e}") + return None + +def _cell_in_validation_range(row: int, col: int, data_validation) -> bool: + """Check if a cell is within a data validation range.""" + try: + # data_validation.sqref contains the cell ranges this validation applies to + for cell_range in data_validation.sqref.ranges: + if (cell_range.min_row <= row <= cell_range.max_row and + cell_range.min_col <= col <= cell_range.max_col): + return True + return False + except Exception as e: + logger.warning(f"Error checking if cell ({row}, {col}) is in validation range for DV sqref '{getattr(data_validation, 'sqref', 'N/A')}': {e}") + return False + +def _extract_validation_metadata(data_validation, cell_address: str, worksheet: Optional[Worksheet] = None) -> Dict[str, Any]: + """Extract metadata from a DataValidation object.""" + try: + validation_info = { + "cell": cell_address, + "has_validation": True, + "validation_type": data_validation.type, + "allow_blank": data_validation.allowBlank, + } + + # Add operator for validation types that use it + if data_validation.operator: + validation_info["operator"] = data_validation.operator + + # Add optional fields if they exist + if data_validation.prompt: + validation_info["prompt"] = data_validation.prompt + if data_validation.promptTitle: + validation_info["prompt_title"] = data_validation.promptTitle + if data_validation.error: + validation_info["error_message"] = data_validation.error + if data_validation.errorTitle: + validation_info["error_title"] = data_validation.errorTitle + + # For list type validations (dropdown lists), extract allowed values + if data_validation.type == "list" and data_validation.formula1: + allowed_values = _extract_list_values(data_validation.formula1, worksheet) + validation_info["allowed_values"] = allowed_values + + # For other validation types, include the formulas + elif data_validation.formula1: + validation_info["formula1"] = data_validation.formula1 + if data_validation.formula2: + validation_info["formula2"] = data_validation.formula2 + + return validation_info + + except Exception as e: + logger.warning(f"Failed to extract validation metadata: {e}") + return { + "cell": cell_address, + "has_validation": True, + "validation_type": "unknown", + "error": f"Failed to parse validation: {e}" + } + +def _extract_list_values(formula: str, worksheet: Optional[Worksheet] = None) -> List[str]: + """Extract allowed values from a list validation formula.""" + try: + # Remove quotes if present + formula = formula.strip('"') + + # Handle comma-separated list + if ',' in formula: + # Split by comma and clean up each value + values = [val.strip().strip('"') for val in formula.split(',')] + return [val for val in values if val] # Remove empty values + + # Handle range reference (e.g., "$A$1:$A$5" or "Sheet1!$A$1:$A$5") + elif (':' in formula or formula.startswith('$')) and worksheet: + try: + # Remove potential leading '=' if it's a formula like '=Sheet1!$A$1:$A$5' + range_ref = formula + if formula.startswith('='): + range_ref = formula[1:] + + actual_values = [] + # worksheet[range_ref] can resolve ranges like "A1:A5" or "SheetName!A1:A5" + # It returns a tuple of tuples of cells for ranges, or a single cell + range_cells = worksheet[range_ref] + + # Handle single cell or range + if hasattr(range_cells, 'value'): # Single cell + if range_cells.value is not None: + actual_values.append(str(range_cells.value)) + else: # Range of cells + for row_of_cells in range_cells: + # Handle case where row_of_cells might be a single cell + if hasattr(row_of_cells, 'value'): + if row_of_cells.value is not None: + actual_values.append(str(row_of_cells.value)) + else: + for cell in row_of_cells: + if cell.value is not None: + actual_values.append(str(cell.value)) + + if actual_values: + return actual_values + return [f"Range: {formula} (empty or unresolvable)"] + + except Exception as e: + logger.warning(f"Could not resolve range '{formula}' for list validation: {e}") + return [f"Range: {formula} (resolution error)"] + + # Handle range reference when worksheet not available + elif ':' in formula or formula.startswith('$'): + return [f"Range: {formula}"] + + # Single value + else: + return [formula.strip('"')] + + except Exception as e: + logger.warning(f"Failed to parse list formula '{formula}': {e}") + return [formula] # Return original formula if parsing fails + +def get_all_validation_ranges(worksheet: Worksheet) -> List[Dict[str, Any]]: + """Get all data validation ranges in a worksheet. + + Returns: + List of dictionaries containing validation range information + """ + validations = [] + + try: + for dv in worksheet.data_validations.dataValidation: + validation_info = { + "ranges": str(dv.sqref), + "validation_type": dv.type, + "allow_blank": dv.allowBlank, + } + + if dv.type == "list" and dv.formula1: + validation_info["allowed_values"] = _extract_list_values(dv.formula1, worksheet) + + validations.append(validation_info) + + except Exception as e: + logger.warning(f"Failed to get validation ranges: {e}") + + return validations \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/chart.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/chart.py new file mode 100644 index 00000000..e92fd6f3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/chart.py @@ -0,0 +1,257 @@ +from typing import Any, Optional, Dict +import logging +from enum import Enum + +from openpyxl import load_workbook +from openpyxl.chart import ( + BarChart, LineChart, PieChart, ScatterChart, + AreaChart, Reference, Series +) +from openpyxl.chart.label import DataLabelList +from openpyxl.chart.legend import Legend +from openpyxl.chart.axis import ChartLines +from openpyxl.drawing.spreadsheet_drawing import ( + AnchorMarker, OneCellAnchor, SpreadsheetDrawing +) +from openpyxl.utils import column_index_from_string + +from .cell_utils import parse_cell_range +from .exceptions import ValidationError, ChartError + +logger = logging.getLogger(__name__) + +class ChartType(str, Enum): + """Supported chart types""" + LINE = "line" + BAR = "bar" + PIE = "pie" + SCATTER = "scatter" + AREA = "area" + BUBBLE = "bubble" + STOCK = "stock" + SURFACE = "surface" + RADAR = "radar" + +class ChartStyle: + """Chart style configuration""" + def __init__( + self, + title_size: int = 14, + title_bold: bool = True, + axis_label_size: int = 12, + show_legend: bool = True, + legend_position: str = "r", + show_data_labels: bool = True, + grid_lines: bool = False, + style_id: int = 2 + ): + self.title_size = title_size + self.title_bold = title_bold + self.axis_label_size = axis_label_size + self.show_legend = show_legend + self.legend_position = legend_position + self.show_data_labels = show_data_labels + self.grid_lines = grid_lines + self.style_id = style_id + +def create_chart_in_sheet( + filepath: str, + sheet_name: str, + data_range: str, + chart_type: str, + target_cell: str, + title: str = "", + x_axis: str = "", + y_axis: str = "", + style: Optional[Dict] = None +) -> dict[str, Any]: + """Create chart in sheet with enhanced styling options""" + # Ensure style dict exists and defaults to showing data labels + if style is None: + style = {"show_data_labels": True} + else: + # If caller omitted the flag, default to True + style.setdefault("show_data_labels", True) + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + logger.error(f"Sheet '{sheet_name}' not found") + raise ValidationError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + # Initialize collections if they don't exist + if not hasattr(worksheet, '_drawings'): + worksheet._drawings = [] + if not hasattr(worksheet, '_charts'): + worksheet._charts = [] + + # Parse the data range + if "!" in data_range: + range_sheet_name, cell_range = data_range.split("!") + if range_sheet_name not in wb.sheetnames: + logger.error(f"Sheet '{range_sheet_name}' referenced in data range not found") + raise ValidationError(f"Sheet '{range_sheet_name}' referenced in data range not found") + else: + cell_range = data_range + + try: + start_cell, end_cell = cell_range.split(":") + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + except ValueError as e: + logger.error(f"Invalid data range format: {e}") + raise ValidationError(f"Invalid data range format: {str(e)}") + + # Validate chart type + chart_classes = { + "line": LineChart, + "bar": BarChart, + "pie": PieChart, + "scatter": ScatterChart, + "area": AreaChart + } + + chart_type_lower = chart_type.lower() + ChartClass = chart_classes.get(chart_type_lower) + if not ChartClass: + logger.error(f"Unsupported chart type: {chart_type}") + raise ValidationError( + f"Unsupported chart type: {chart_type}. " + f"Supported types: {', '.join(chart_classes.keys())}" + ) + + chart = ChartClass() + + # Basic chart settings + chart.title = title + if hasattr(chart, "x_axis"): + chart.x_axis.title = x_axis + if hasattr(chart, "y_axis"): + chart.y_axis.title = y_axis + + try: + # Create data references + if chart_type_lower == "scatter": + # For scatter charts, create series for each pair of columns + for col in range(start_col + 1, end_col + 1): + x_values = Reference( + worksheet, + min_row=start_row + 1, + max_row=end_row, + min_col=start_col + ) + y_values = Reference( + worksheet, + min_row=start_row + 1, + max_row=end_row, + min_col=col + ) + series = Series(y_values, x_values, title_from_data=True) + chart.series.append(series) + else: + # For other chart types + data = Reference( + worksheet, + min_row=start_row, + max_row=end_row, + min_col=start_col + 1, + max_col=end_col + ) + cats = Reference( + worksheet, + min_row=start_row + 1, + max_row=end_row, + min_col=start_col + ) + chart.add_data(data, titles_from_data=True) + chart.set_categories(cats) + except Exception as e: + logger.error(f"Failed to create chart data references: {e}") + raise ChartError(f"Failed to create chart data references: {str(e)}") + + # Apply style if provided + try: + if style.get("show_legend", True): + chart.legend = Legend() + chart.legend.position = style.get("legend_position", "r") + else: + chart.legend = None + + if style.get("show_data_labels", False): + data_labels = DataLabelList() + # Gather optional overrides + dlo = style.get("data_label_options", {}) if isinstance(style.get("data_label_options", {}), dict) else {} + + # Helper to read bool with fallback + def _opt(name: str, default: bool) -> bool: + return bool(dlo.get(name, default)) + + # Apply options – Excel will concatenate any that are set to True + data_labels.showVal = _opt("show_val", True) + data_labels.showCatName = _opt("show_cat_name", False) + data_labels.showSerName = _opt("show_ser_name", False) + data_labels.showLegendKey = _opt("show_legend_key", False) + data_labels.showPercent = _opt("show_percent", False) + data_labels.showBubbleSize = _opt("show_bubble_size", False) + + chart.dataLabels = data_labels + + if style.get("grid_lines", False): + if hasattr(chart, "x_axis"): + chart.x_axis.majorGridlines = ChartLines() + if hasattr(chart, "y_axis"): + chart.y_axis.majorGridlines = ChartLines() + except Exception as e: + logger.error(f"Failed to apply chart style: {e}") + raise ChartError(f"Failed to apply chart style: {str(e)}") + + # Set chart size + chart.width = 15 + chart.height = 7.5 + + # Create drawing and anchor + try: + drawing = SpreadsheetDrawing() + drawing.chart = chart + + # Validate target cell format + if not target_cell or not any(c.isalpha() for c in target_cell) or not any(c.isdigit() for c in target_cell): + raise ValidationError(f"Invalid target cell format: {target_cell}") + + # Create anchor + col = column_index_from_string(target_cell[0]) - 1 + row = int(target_cell[1:]) - 1 + anchor = OneCellAnchor() + anchor._from = AnchorMarker(col=col, row=row) + drawing.anchor = anchor + + # Add to worksheet + worksheet._drawings.append(drawing) + worksheet._charts.append(chart) + except ValueError as e: + logger.error(f"Invalid target cell: {e}") + raise ValidationError(f"Invalid target cell: {str(e)}") + except Exception as e: + logger.error(f"Failed to create chart drawing: {e}") + raise ChartError(f"Failed to create chart drawing: {str(e)}") + + try: + wb.save(filepath) + except Exception as e: + logger.error(f"Failed to save workbook: {e}") + raise ChartError(f"Failed to save workbook with chart: {str(e)}") + + return { + "message": f"{chart_type.capitalize()} chart created successfully", + "details": { + "type": chart_type, + "location": target_cell, + "data_range": data_range + } + } + + except (ValidationError, ChartError): + raise + except Exception as e: + logger.error(f"Unexpected error creating chart: {e}") + raise ChartError(f"Unexpected error creating chart: {str(e)}") diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/data.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/data.py new file mode 100644 index 00000000..b0b9b86e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/data.py @@ -0,0 +1,280 @@ +from pathlib import Path +from typing import Any, Dict, List, Optional +import logging + +from openpyxl import load_workbook +from openpyxl.worksheet.worksheet import Worksheet +from openpyxl.utils import get_column_letter + +from .exceptions import DataError +from .cell_utils import parse_cell_range +from .cell_validation import get_data_validation_for_cell + +logger = logging.getLogger(__name__) + +def read_excel_range( + filepath: Path | str, + sheet_name: str, + start_cell: str = "A1", + end_cell: Optional[str] = None, + preview_only: bool = False +) -> List[Dict[str, Any]]: + """Read data from Excel range with optional preview mode""" + try: + wb = load_workbook(filepath, read_only=False) + + if sheet_name not in wb.sheetnames: + raise DataError(f"Sheet '{sheet_name}' not found") + + ws = wb[sheet_name] + + # Parse start cell + if ':' in start_cell: + start_cell, end_cell = start_cell.split(':') + + # Get start coordinates + try: + start_coords = parse_cell_range(f"{start_cell}:{start_cell}") + if not start_coords or not all(coord is not None for coord in start_coords[:2]): + raise DataError(f"Invalid start cell reference: {start_cell}") + start_row, start_col = start_coords[0], start_coords[1] + except ValueError as e: + raise DataError(f"Invalid start cell format: {str(e)}") + + # Determine end coordinates + if end_cell: + try: + end_coords = parse_cell_range(f"{end_cell}:{end_cell}") + if not end_coords or not all(coord is not None for coord in end_coords[:2]): + raise DataError(f"Invalid end cell reference: {end_cell}") + end_row, end_col = end_coords[0], end_coords[1] + except ValueError as e: + raise DataError(f"Invalid end cell format: {str(e)}") + else: + # If no end_cell, use the full data range of the sheet + if ws.max_row == 1 and ws.max_column == 1 and ws.cell(1, 1).value is None: + # Handle empty sheet + end_row, end_col = start_row, start_col + else: + # Use the sheet's own boundaries + start_row, start_col = ws.min_row, ws.min_column + end_row, end_col = ws.max_row, ws.max_column + + # Validate range bounds + if start_row > ws.max_row or start_col > ws.max_column: + # This case can happen if start_cell is outside the used area on a sheet with data + # or on a completely empty sheet. + logger.warning( + f"Start cell {start_cell} is outside the sheet's data boundary " + f"({get_column_letter(ws.min_column)}{ws.min_row}:{get_column_letter(ws.max_column)}{ws.max_row}). " + f"No data will be read." + ) + return [] + + data = [] + for row in range(start_row, end_row + 1): + row_data = [] + for col in range(start_col, end_col + 1): + cell = ws.cell(row=row, column=col) + row_data.append(cell.value) + if any(v is not None for v in row_data): + data.append(row_data) + + wb.close() + return data + except DataError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to read Excel range: {e}") + raise DataError(str(e)) + +def write_data( + filepath: str, + sheet_name: Optional[str], + data: Optional[List[List]], + start_cell: str = "A1", +) -> Dict[str, str]: + """Write data to Excel sheet with workbook handling + + Headers are handled intelligently based on context. + """ + try: + if not data: + raise DataError("No data provided to write") + + wb = load_workbook(filepath) + + # If no sheet specified, use active sheet + if not sheet_name: + active_sheet = wb.active + if active_sheet is None: + raise DataError("No active sheet found in workbook") + sheet_name = active_sheet.title + elif sheet_name not in wb.sheetnames: + wb.create_sheet(sheet_name) + + ws = wb[sheet_name] + + # Validate start cell + try: + start_coords = parse_cell_range(start_cell) + if not start_coords or not all(coord is not None for coord in start_coords[:2]): + raise DataError(f"Invalid start cell reference: {start_cell}") + except ValueError as e: + raise DataError(f"Invalid start cell format: {str(e)}") + + if len(data) > 0: + _write_data_to_worksheet(ws, data, start_cell) + + wb.save(filepath) + wb.close() + + return {"message": f"Data written to {sheet_name}", "active_sheet": sheet_name} + except DataError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to write data: {e}") + raise DataError(str(e)) + +def _write_data_to_worksheet( + worksheet: Worksheet, + data: List[List], + start_cell: str = "A1", +) -> None: + """Write data to worksheet with intelligent header handling""" + try: + if not data: + raise DataError("No data provided to write") + + try: + start_coords = parse_cell_range(start_cell) + if not start_coords or not all(x is not None for x in start_coords[:2]): + raise DataError(f"Invalid start cell reference: {start_cell}") + start_row, start_col = start_coords[0], start_coords[1] + except ValueError as e: + raise DataError(f"Invalid start cell format: {str(e)}") + + # Write data + for i, row in enumerate(data): + for j, val in enumerate(row): + worksheet.cell(row=start_row + i, column=start_col + j, value=val) + except DataError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to write worksheet data: {e}") + raise DataError(str(e)) + +def read_excel_range_with_metadata( + filepath: Path | str, + sheet_name: str, + start_cell: str = "A1", + end_cell: Optional[str] = None, + include_validation: bool = True +) -> Dict[str, Any]: + """Read data from Excel range with cell metadata including validation rules. + + Args: + filepath: Path to Excel file + sheet_name: Name of worksheet + start_cell: Starting cell address + end_cell: Ending cell address (optional) + include_validation: Whether to include validation metadata + + Returns: + Dictionary containing structured cell data with metadata + """ + try: + wb = load_workbook(filepath, read_only=False) + + if sheet_name not in wb.sheetnames: + raise DataError(f"Sheet '{sheet_name}' not found") + + ws = wb[sheet_name] + + # Parse start cell + if ':' in start_cell: + start_cell, end_cell = start_cell.split(':') + + # Get start coordinates + try: + start_coords = parse_cell_range(f"{start_cell}:{start_cell}") + if not start_coords or not all(coord is not None for coord in start_coords[:2]): + raise DataError(f"Invalid start cell reference: {start_cell}") + start_row, start_col = start_coords[0], start_coords[1] + except ValueError as e: + raise DataError(f"Invalid start cell format: {str(e)}") + + # Determine end coordinates + if end_cell: + try: + end_coords = parse_cell_range(f"{end_cell}:{end_cell}") + if not end_coords or not all(coord is not None for coord in end_coords[:2]): + raise DataError(f"Invalid end cell reference: {end_cell}") + end_row, end_col = end_coords[0], end_coords[1] + except ValueError as e: + raise DataError(f"Invalid end cell format: {str(e)}") + else: + # If no end_cell, use the full data range of the sheet + if ws.max_row == 1 and ws.max_column == 1 and ws.cell(1, 1).value is None: + # Handle empty sheet + end_row, end_col = start_row, start_col + else: + # Use the sheet's own boundaries, but respect the provided start_cell + end_row, end_col = ws.max_row, ws.max_column + # If start_cell is 'A1' (default), we should find the true start + if start_cell == 'A1': + start_row, start_col = ws.min_row, ws.min_column + + # Validate range bounds + if start_row > ws.max_row or start_col > ws.max_column: + # This case can happen if start_cell is outside the used area on a sheet with data + # or on a completely empty sheet. + logger.warning( + f"Start cell {start_cell} is outside the sheet's data boundary " + f"({get_column_letter(ws.min_column)}{ws.min_row}:{get_column_letter(ws.max_column)}{ws.max_row}). " + f"No data will be read." + ) + return {"range": f"{start_cell}:", "sheet_name": sheet_name, "cells": []} + + # Build structured cell data + range_str = f"{get_column_letter(start_col)}{start_row}:{get_column_letter(end_col)}{end_row}" + range_data = { + "range": range_str, + "sheet_name": sheet_name, + "cells": [] + } + + for row in range(start_row, end_row + 1): + for col in range(start_col, end_col + 1): + cell = ws.cell(row=row, column=col) + cell_address = f"{get_column_letter(col)}{row}" + + cell_data = { + "address": cell_address, + "value": cell.value, + "row": row, + "column": col + } + + # Add validation metadata if requested + if include_validation: + validation_info = get_data_validation_for_cell(ws, cell_address) + if validation_info: + cell_data["validation"] = validation_info + else: + cell_data["validation"] = {"has_validation": False} + + range_data["cells"].append(cell_data) + + wb.close() + return range_data + + except DataError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to read Excel range with metadata: {e}") + raise DataError(str(e)) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/exceptions.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/exceptions.py new file mode 100644 index 00000000..80d1afd6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/exceptions.py @@ -0,0 +1,35 @@ +class ExcelMCPError(Exception): + """Base exception for Excel MCP errors.""" + pass + +class WorkbookError(ExcelMCPError): + """Raised when workbook operations fail.""" + pass + +class SheetError(ExcelMCPError): + """Raised when sheet operations fail.""" + pass + +class DataError(ExcelMCPError): + """Raised when data operations fail.""" + pass + +class ValidationError(ExcelMCPError): + """Raised when validation fails.""" + pass + +class FormattingError(ExcelMCPError): + """Raised when formatting operations fail.""" + pass + +class CalculationError(ExcelMCPError): + """Raised when formula calculations fail.""" + pass + +class PivotError(ExcelMCPError): + """Raised when pivot table operations fail.""" + pass + +class ChartError(ExcelMCPError): + """Raised when chart operations fail.""" + pass diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/formatting.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/formatting.py new file mode 100644 index 00000000..34dd8398 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/formatting.py @@ -0,0 +1,249 @@ +import logging +from typing import Any, Dict, Optional + +from openpyxl.styles import ( + PatternFill, Border, Side, Alignment, Protection, Font, + Color +) +from openpyxl.formatting.rule import ( + ColorScaleRule, DataBarRule, IconSetRule, + FormulaRule, CellIsRule +) + +from .workbook import get_or_create_workbook +from .cell_utils import parse_cell_range, validate_cell_reference +from .exceptions import ValidationError, FormattingError + +logger = logging.getLogger(__name__) + +def format_range( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: Optional[str] = None, + bold: bool = False, + italic: bool = False, + underline: bool = False, + font_size: Optional[int] = None, + font_color: Optional[str] = None, + bg_color: Optional[str] = None, + border_style: Optional[str] = None, + border_color: Optional[str] = None, + number_format: Optional[str] = None, + alignment: Optional[str] = None, + wrap_text: bool = False, + merge_cells: bool = False, + protection: Optional[Dict[str, Any]] = None, + conditional_format: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """Apply formatting to a range of cells. + + This function handles all Excel formatting operations including: + - Font properties (bold, italic, size, color, etc.) + - Cell fill/background color + - Borders (style and color) + - Number formatting + - Alignment and text wrapping + - Cell merging + - Protection + - Conditional formatting + + Args: + filepath: Path to Excel file + sheet_name: Name of worksheet + start_cell: Starting cell reference + end_cell: Optional ending cell reference + bold: Whether to make text bold + italic: Whether to make text italic + underline: Whether to underline text + font_size: Font size in points + font_color: Font color (hex code) + bg_color: Background color (hex code) + border_style: Border style (thin, medium, thick, double) + border_color: Border color (hex code) + number_format: Excel number format string + alignment: Text alignment (left, center, right, justify) + wrap_text: Whether to wrap text + merge_cells: Whether to merge the range + protection: Cell protection settings + conditional_format: Conditional formatting rules + + Returns: + Dictionary with operation status + """ + try: + # Validate cell references + if not validate_cell_reference(start_cell): + raise ValidationError(f"Invalid start cell reference: {start_cell}") + + if end_cell and not validate_cell_reference(end_cell): + raise ValidationError(f"Invalid end cell reference: {end_cell}") + + wb = get_or_create_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise ValidationError(f"Sheet '{sheet_name}' not found") + + sheet = wb[sheet_name] + + # Get cell range coordinates + try: + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + except ValueError as e: + raise ValidationError(f"Invalid cell range: {str(e)}") + + # If no end cell specified, use start cell coordinates + if end_row is None: + end_row = start_row + if end_col is None: + end_col = start_col + + # Apply font formatting + font_args = { + "bold": bold, + "italic": italic, + "underline": 'single' if underline else None, + } + if font_size is not None: + font_args["size"] = font_size + if font_color is not None: + try: + # Ensure color has FF prefix for full opacity + font_color = font_color if font_color.startswith('FF') else f'FF{font_color}' + font_args["color"] = Color(rgb=font_color) + except ValueError as e: + raise FormattingError(f"Invalid font color: {str(e)}") + font = Font(**font_args) + + # Apply fill + fill = None + if bg_color is not None: + try: + # Ensure color has FF prefix for full opacity + bg_color = bg_color if bg_color.startswith('FF') else f'FF{bg_color}' + fill = PatternFill( + start_color=Color(rgb=bg_color), + end_color=Color(rgb=bg_color), + fill_type='solid' + ) + except ValueError as e: + raise FormattingError(f"Invalid background color: {str(e)}") + + # Apply borders + border = None + if border_style is not None: + try: + border_color = border_color if border_color else "000000" + border_color = border_color if border_color.startswith('FF') else f'FF{border_color}' + side = Side( + style=border_style, + color=Color(rgb=border_color) + ) + border = Border( + left=side, + right=side, + top=side, + bottom=side + ) + except ValueError as e: + raise FormattingError(f"Invalid border settings: {str(e)}") + + # Apply alignment + align = None + if alignment is not None or wrap_text: + try: + align = Alignment( + horizontal=alignment, + vertical='center', + wrap_text=wrap_text + ) + except ValueError as e: + raise FormattingError(f"Invalid alignment settings: {str(e)}") + + # Apply protection + protect = None + if protection is not None: + try: + protect = Protection(**protection) + except ValueError as e: + raise FormattingError(f"Invalid protection settings: {str(e)}") + + # Apply formatting to range + for row in range(start_row, end_row + 1): + for col in range(start_col, end_col + 1): + cell = sheet.cell(row=row, column=col) + cell.font = font + if fill is not None: + cell.fill = fill + if border is not None: + cell.border = border + if align is not None: + cell.alignment = align + if protect is not None: + cell.protection = protect + if number_format is not None: + cell.number_format = number_format + + # Merge cells if requested + if merge_cells and end_cell: + try: + range_str = f"{start_cell}:{end_cell}" + sheet.merge_cells(range_str) + except ValueError as e: + raise FormattingError(f"Failed to merge cells: {str(e)}") + + # Apply conditional formatting + if conditional_format is not None: + range_str = f"{start_cell}:{end_cell}" if end_cell else start_cell + rule_type = conditional_format.get('type') + if not rule_type: + raise FormattingError("Conditional format type not specified") + + params = conditional_format.get('params', {}) + + # Handle fill parameter for cell_is rule + if rule_type == 'cell_is' and 'fill' in params: + fill_params = params['fill'] + if isinstance(fill_params, dict): + try: + fill_color = fill_params.get('fgColor', 'FFC7CE') # Default to light red + fill_color = fill_color if fill_color.startswith('FF') else f'FF{fill_color}' + params['fill'] = PatternFill( + start_color=fill_color, + end_color=fill_color, + fill_type='solid' + ) + except ValueError as e: + raise FormattingError(f"Invalid conditional format fill color: {str(e)}") + + try: + if rule_type == 'color_scale': + rule = ColorScaleRule(**params) + elif rule_type == 'data_bar': + rule = DataBarRule(**params) + elif rule_type == 'icon_set': + rule = IconSetRule(**params) + elif rule_type == 'formula': + rule = FormulaRule(**params) + elif rule_type == 'cell_is': + rule = CellIsRule(**params) + else: + raise FormattingError(f"Invalid conditional format type: {rule_type}") + + sheet.conditional_formatting.add(range_str, rule) + except Exception as e: + raise FormattingError(f"Failed to apply conditional formatting: {str(e)}") + + wb.save(filepath) + + range_str = f"{start_cell}:{end_cell}" if end_cell else start_cell + return { + "message": f"Applied formatting to range {range_str}", + "range": range_str + } + + except (ValidationError, FormattingError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to apply formatting: {e}") + raise FormattingError(str(e)) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/pivot.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/pivot.py new file mode 100644 index 00000000..9e141d0b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/pivot.py @@ -0,0 +1,270 @@ +from typing import Any +import uuid +import logging + +from openpyxl import load_workbook +from openpyxl.utils import get_column_letter +from openpyxl.worksheet.table import Table, TableStyleInfo +from openpyxl.styles import Font + +from .data import read_excel_range +from .cell_utils import parse_cell_range +from .exceptions import ValidationError, PivotError + +logger = logging.getLogger(__name__) + +def create_pivot_table( + filepath: str, + sheet_name: str, + data_range: str, + rows: list[str], + values: list[str], + columns: list[str] | None = None, + agg_func: str = "sum" +) -> dict[str, Any]: + """Create pivot table in sheet using Excel table functionality + + Args: + filepath: Path to Excel file + sheet_name: Name of worksheet containing source data + data_range: Source data range reference + target_cell: Cell reference for pivot table position + rows: Fields for row labels + values: Fields for values + columns: Optional fields for column labels + agg_func: Aggregation function (sum, count, average, max, min) + + Returns: + Dictionary with status message and pivot table dimensions + """ + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise ValidationError(f"Sheet '{sheet_name}' not found") + + # Parse ranges + if ':' not in data_range: + raise ValidationError("Data range must be in format 'A1:B2'") + + try: + start_cell, end_cell = data_range.split(':') + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + except ValueError as e: + raise ValidationError(f"Invalid data range format: {str(e)}") + + if end_row is None or end_col is None: + raise ValidationError("Invalid data range format: missing end coordinates") + + # Create range string + data_range_str = f"{get_column_letter(start_col)}{start_row}:{get_column_letter(end_col)}{end_row}" + + # Clean up field names by removing aggregation suffixes + def clean_field_name(field: str) -> str: + field = str(field).strip() + for suffix in [" (sum)", " (average)", " (count)", " (min)", " (max)"]: + if field.lower().endswith(suffix): + return field[:-len(suffix)] + return field + + # Read source data and convert to list of dicts + try: + data_as_list = read_excel_range(filepath, sheet_name, start_cell, end_cell) + if not data_as_list or len(data_as_list) < 2: + raise PivotError("Source data must have a header row and at least one data row.") + + headers = [str(h) for h in data_as_list[0]] + data = [dict(zip(headers, row)) for row in data_as_list[1:]] + + if not data: + raise PivotError("No data rows found after header.") + + except Exception as e: + raise PivotError(f"Failed to read or process source data: {str(e)}") + + # Validate aggregation function + valid_agg_funcs = ["sum", "average", "count", "min", "max"] + if agg_func.lower() not in valid_agg_funcs: + raise ValidationError( + f"Invalid aggregation function. Must be one of: {', '.join(valid_agg_funcs)}" + ) + + # Validate field names exist in data + if data: + available_fields_raw = data[0].keys() + available_fields = {clean_field_name(str(header)).lower() for header in available_fields_raw} + + for field_list, field_type in [(rows, "row"), (values, "value")]: + for field in field_list: + if clean_field_name(str(field)).lower() not in available_fields: + raise ValidationError( + f"Invalid {field_type} field '{field}'. " + f"Available fields: {', '.join(sorted(available_fields_raw))}" + ) + + if columns: + for field in columns: + if clean_field_name(str(field)).lower() not in available_fields: + raise ValidationError( + f"Invalid column field '{field}'. " + f"Available fields: {', '.join(sorted(available_fields_raw))}" + ) + + # Clean up row and value field names + cleaned_rows = [clean_field_name(field) for field in rows] + cleaned_values = [clean_field_name(field) for field in values] + + # Create pivot sheet + pivot_sheet_name = f"{sheet_name}_pivot" + if pivot_sheet_name in wb.sheetnames: + wb.remove(wb[pivot_sheet_name]) + pivot_ws = wb.create_sheet(pivot_sheet_name) + + # Write headers + current_row = 1 + current_col = 1 + + # Write row field headers + for field in cleaned_rows: + cell = pivot_ws.cell(row=current_row, column=current_col, value=field) + cell.font = Font(bold=True) + current_col += 1 + + # Write value field headers + for field in cleaned_values: + cell = pivot_ws.cell(row=current_row, column=current_col, value=f"{field} ({agg_func})") + cell.font = Font(bold=True) + current_col += 1 + + # Get unique values for each row field + field_values = {} + for field in cleaned_rows: + all_values = [] + for record in data: + value = str(record.get(field, '')) + all_values.append(value) + field_values[field] = sorted(set(all_values)) + + # Generate all combinations of row field values + row_combinations = _get_combinations(field_values) + + # Calculate table dimensions for formatting + total_rows = len(row_combinations) + 1 # +1 for header + total_cols = len(cleaned_rows) + len(cleaned_values) + + # Write data rows + current_row = 2 + for combo in row_combinations: + # Write row field values + col = 1 + for field in cleaned_rows: + pivot_ws.cell(row=current_row, column=col, value=combo[field]) + col += 1 + + # Filter data for current combination + filtered_data = _filter_data(data, combo, {}) + + # Calculate and write aggregated values + for value_field in cleaned_values: + try: + value = _aggregate_values(filtered_data, value_field, agg_func) + pivot_ws.cell(row=current_row, column=col, value=value) + except Exception as e: + raise PivotError(f"Failed to aggregate values for field '{value_field}': {str(e)}") + col += 1 + + current_row += 1 + + # Create a table for the pivot data + try: + pivot_range = f"A1:{get_column_letter(total_cols)}{total_rows}" + pivot_table = Table( + displayName=f"PivotTable_{uuid.uuid4().hex[:8]}", + ref=pivot_range + ) + style = TableStyleInfo( + name="TableStyleMedium9", + showFirstColumn=False, + showLastColumn=False, + showRowStripes=True, + showColumnStripes=True + ) + pivot_table.tableStyleInfo = style + pivot_ws.add_table(pivot_table) + except Exception as e: + raise PivotError(f"Failed to create pivot table formatting: {str(e)}") + + try: + wb.save(filepath) + except Exception as e: + raise PivotError(f"Failed to save workbook: {str(e)}") + + return { + "message": "Summary table created successfully", + "details": { + "source_range": data_range_str, + "pivot_sheet": pivot_sheet_name, + "rows": cleaned_rows, + "columns": columns or [], + "values": cleaned_values, + "aggregation": agg_func + } + } + + except (ValidationError, PivotError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to create pivot table: {e}") + raise PivotError(str(e)) + + +def _get_combinations(field_values: dict[str, set]) -> list[dict]: + """Get all combinations of field values.""" + result = [{}] + for field, values in list(field_values.items()): # Convert to list to avoid runtime changes + new_result = [] + for combo in result: + for value in sorted(values): # Sort for consistent ordering + new_combo = combo.copy() + new_combo[field] = value + new_result.append(new_combo) + result = new_result + return result + + +def _filter_data(data: list[dict], row_filters: dict, col_filters: dict) -> list[dict]: + """Filter data based on row and column filters.""" + result = [] + for record in data: + matches = True + for field, value in row_filters.items(): + if record.get(field) != value: + matches = False + break + for field, value in col_filters.items(): + if record.get(field) != value: + matches = False + break + if matches: + result.append(record) + return result + + +def _aggregate_values(data: list[dict], field: str, agg_func: str) -> float: + """Aggregate values using the specified function.""" + values = [record[field] for record in data if field in record and isinstance(record[field], (int, float))] + if not values: + return 0 + + if agg_func == "sum": + return sum(values) + elif agg_func == "average": + return sum(values) / len(values) + elif agg_func == "count": + return len(values) + elif agg_func == "min": + return min(values) + elif agg_func == "max": + return max(values) + else: + return sum(values) # Default to sum diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/server.py new file mode 100644 index 00000000..dcfccf9b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/server.py @@ -0,0 +1,848 @@ +import logging +import os +from typing import Any, List, Dict, Optional + +from mcp.server.fastmcp import FastMCP +from mcp.types import ToolAnnotations + +# Import exceptions +from excel_mcp.exceptions import ( + ValidationError, + WorkbookError, + SheetError, + DataError, + FormattingError, + CalculationError, + PivotError, + ChartError +) + +# Import from excel_mcp package with consistent _impl suffixes +from excel_mcp.validation import ( + validate_formula_in_cell_operation as validate_formula_impl, + validate_range_in_sheet_operation as validate_range_impl +) +from excel_mcp.chart import create_chart_in_sheet as create_chart_impl +from excel_mcp.workbook import get_workbook_info +from excel_mcp.data import write_data +from excel_mcp.pivot import create_pivot_table as create_pivot_table_impl +from excel_mcp.tables import create_excel_table as create_table_impl +from excel_mcp.sheet import ( + copy_sheet, + delete_sheet, + rename_sheet, + merge_range, + unmerge_range, + get_merged_ranges, + insert_row, + insert_cols, + delete_rows, + delete_cols, +) + +# Get project root directory path for log file path. +# When using the stdio transmission method, +# relative paths may cause log files to fail to create +# due to the client's running location and permission issues, +# resulting in the program not being able to run. +# Thus using os.path.join(ROOT_DIR, "excel-mcp.log") instead. + +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +LOG_FILE = os.path.join(ROOT_DIR, "excel-mcp.log") + +# Initialize EXCEL_FILES_PATH variable without assigning a value +EXCEL_FILES_PATH = None + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + # Referring to https://github.com/modelcontextprotocol/python-sdk/issues/409#issuecomment-2816831318 + # The stdio mode server MUST NOT write anything to its stdout that is not a valid MCP message. + logging.FileHandler(LOG_FILE) + ], +) +logger = logging.getLogger("excel-mcp") +# Initialize FastMCP server +mcp = FastMCP( + "excel-mcp", + host=os.environ.get("FASTMCP_HOST", "0.0.0.0"), + port=int(os.environ.get("FASTMCP_PORT", "8017")), + instructions="Excel MCP Server for manipulating Excel files" +) + +def get_excel_path(filename: str) -> str: + """Get full path to Excel file. + + Args: + filename: Name of Excel file + + Returns: + Full path to Excel file + """ + # If filename is already an absolute path, return it + if os.path.isabs(filename): + return filename + + # Check if in SSE mode (EXCEL_FILES_PATH is not None) + if EXCEL_FILES_PATH is None: + # Must use absolute path + raise ValueError(f"Invalid filename: {filename}, must be an absolute path when not in SSE mode") + + # In SSE mode, if it's a relative path, resolve it based on EXCEL_FILES_PATH + return os.path.join(EXCEL_FILES_PATH, filename) + +@mcp.tool( + annotations=ToolAnnotations( + title="Apply Formula", + destructiveHint=True, + ), +) +def apply_formula( + filepath: str, + sheet_name: str, + cell: str, + formula: str, +) -> str: + """ + Apply Excel formula to cell. + Excel formula will write to cell with verification. + """ + try: + full_path = get_excel_path(filepath) + # First validate the formula + validation = validate_formula_impl(full_path, sheet_name, cell, formula) + if isinstance(validation, dict) and "error" in validation: + return f"Error: {validation['error']}" + + # If valid, apply the formula + from excel_mcp.calculations import apply_formula as apply_formula_impl + result = apply_formula_impl(full_path, sheet_name, cell, formula) + return result["message"] + except (ValidationError, CalculationError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error applying formula: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Validate Formula Syntax", + readOnlyHint=True, + ), +) +def validate_formula_syntax( + filepath: str, + sheet_name: str, + cell: str, + formula: str, +) -> str: + """Validate Excel formula syntax without applying it.""" + try: + full_path = get_excel_path(filepath) + result = validate_formula_impl(full_path, sheet_name, cell, formula) + return result["message"] + except (ValidationError, CalculationError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error validating formula: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Format Range", + destructiveHint=True, + ), +) +def format_range( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: Optional[str] = None, + bold: bool = False, + italic: bool = False, + underline: bool = False, + font_size: Optional[int] = None, + font_color: Optional[str] = None, + bg_color: Optional[str] = None, + border_style: Optional[str] = None, + border_color: Optional[str] = None, + number_format: Optional[str] = None, + alignment: Optional[str] = None, + wrap_text: bool = False, + merge_cells: bool = False, + protection: Optional[Dict[str, Any]] = None, + conditional_format: Optional[Dict[str, Any]] = None +) -> str: + """Apply formatting to a range of cells.""" + try: + full_path = get_excel_path(filepath) + from excel_mcp.formatting import format_range as format_range_func + + # Convert None values to appropriate defaults for the underlying function + format_range_func( + filepath=full_path, + sheet_name=sheet_name, + start_cell=start_cell, + end_cell=end_cell, # This can be None + bold=bold, + italic=italic, + underline=underline, + font_size=font_size, # This can be None + font_color=font_color, # This can be None + bg_color=bg_color, # This can be None + border_style=border_style, # This can be None + border_color=border_color, # This can be None + number_format=number_format, # This can be None + alignment=alignment, # This can be None + wrap_text=wrap_text, + merge_cells=merge_cells, + protection=protection, # This can be None + conditional_format=conditional_format # This can be None + ) + return "Range formatted successfully" + except (ValidationError, FormattingError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error formatting range: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Read Data from Excel", + readOnlyHint=True, + ), +) +def read_data_from_excel( + filepath: str, + sheet_name: str, + start_cell: str = "A1", + end_cell: Optional[str] = None, + preview_only: bool = False +) -> str: + """ + Read data from Excel worksheet with cell metadata including validation rules. + + Args: + filepath: Path to Excel file + sheet_name: Name of worksheet + start_cell: Starting cell (default A1) + end_cell: Ending cell (optional, auto-expands if not provided) + preview_only: Whether to return preview only + + Returns: + JSON string containing structured cell data with validation metadata. + Each cell includes: address, value, row, column, and validation info (if any). + """ + try: + full_path = get_excel_path(filepath) + from excel_mcp.data import read_excel_range_with_metadata + result = read_excel_range_with_metadata( + full_path, + sheet_name, + start_cell, + end_cell + ) + if not result or not result.get("cells"): + return "No data found in specified range" + + # Return as formatted JSON string + import json + return json.dumps(result, indent=2, default=str) + + except Exception as e: + logger.error(f"Error reading data: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Write Data to Excel", + destructiveHint=True, + ), +) +def write_data_to_excel( + filepath: str, + sheet_name: str, + data: List[List], + start_cell: str = "A1", +) -> str: + """ + Write data to Excel worksheet. + Excel formula will write to cell without any verification. + + PARAMETERS: + filepath: Path to Excel file + sheet_name: Name of worksheet to write to + data: List of lists containing data to write to the worksheet, sublists are assumed to be rows + start_cell: Cell to start writing to, default is "A1" + + """ + try: + full_path = get_excel_path(filepath) + result = write_data(full_path, sheet_name, data, start_cell) + return result["message"] + except (ValidationError, DataError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error writing data: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Create Workbook", + destructiveHint=True, + ), +) +def create_workbook(filepath: str) -> str: + """Create new Excel workbook.""" + try: + full_path = get_excel_path(filepath) + from excel_mcp.workbook import create_workbook as create_workbook_impl + create_workbook_impl(full_path) + return f"Created workbook at {full_path}" + except WorkbookError as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error creating workbook: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Create Worksheet", + destructiveHint=True, + ), +) +def create_worksheet(filepath: str, sheet_name: str) -> str: + """Create new worksheet in workbook.""" + try: + full_path = get_excel_path(filepath) + from excel_mcp.workbook import create_sheet as create_worksheet_impl + result = create_worksheet_impl(full_path, sheet_name) + return result["message"] + except (ValidationError, WorkbookError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error creating worksheet: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Create Chart", + destructiveHint=True, + ), +) +def create_chart( + filepath: str, + sheet_name: str, + data_range: str, + chart_type: str, + target_cell: str, + title: str = "", + x_axis: str = "", + y_axis: str = "" +) -> str: + """Create chart in worksheet.""" + try: + full_path = get_excel_path(filepath) + result = create_chart_impl( + filepath=full_path, + sheet_name=sheet_name, + data_range=data_range, + chart_type=chart_type, + target_cell=target_cell, + title=title, + x_axis=x_axis, + y_axis=y_axis + ) + return result["message"] + except (ValidationError, ChartError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error creating chart: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Create Pivot Table", + destructiveHint=True, + ), +) +def create_pivot_table( + filepath: str, + sheet_name: str, + data_range: str, + rows: List[str], + values: List[str], + columns: Optional[List[str]] = None, + agg_func: str = "mean" +) -> str: + """Create pivot table in worksheet.""" + try: + full_path = get_excel_path(filepath) + result = create_pivot_table_impl( + filepath=full_path, + sheet_name=sheet_name, + data_range=data_range, + rows=rows, + values=values, + columns=columns or [], + agg_func=agg_func + ) + return result["message"] + except (ValidationError, PivotError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error creating pivot table: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Create Table", + destructiveHint=True, + ), +) +def create_table( + filepath: str, + sheet_name: str, + data_range: str, + table_name: Optional[str] = None, + table_style: str = "TableStyleMedium9" +) -> str: + """Creates a native Excel table from a specified range of data.""" + try: + full_path = get_excel_path(filepath) + result = create_table_impl( + filepath=full_path, + sheet_name=sheet_name, + data_range=data_range, + table_name=table_name, + table_style=table_style + ) + return result["message"] + except DataError as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error creating table: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Copy Worksheet", + destructiveHint=True, + ), +) +def copy_worksheet( + filepath: str, + source_sheet: str, + target_sheet: str +) -> str: + """Copy worksheet within workbook.""" + try: + full_path = get_excel_path(filepath) + result = copy_sheet(full_path, source_sheet, target_sheet) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error copying worksheet: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Delete Worksheet", + destructiveHint=True, + ), +) +def delete_worksheet( + filepath: str, + sheet_name: str +) -> str: + """Delete worksheet from workbook.""" + try: + full_path = get_excel_path(filepath) + result = delete_sheet(full_path, sheet_name) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error deleting worksheet: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Rename Worksheet", + destructiveHint=True, + ), +) +def rename_worksheet( + filepath: str, + old_name: str, + new_name: str +) -> str: + """Rename worksheet in workbook.""" + try: + full_path = get_excel_path(filepath) + result = rename_sheet(full_path, old_name, new_name) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error renaming worksheet: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Get Workbook Metadata", + readOnlyHint=True, + ), +) +def get_workbook_metadata( + filepath: str, + include_ranges: bool = False +) -> str: + """Get metadata about workbook including sheets, ranges, etc.""" + try: + full_path = get_excel_path(filepath) + result = get_workbook_info(full_path, include_ranges=include_ranges) + return str(result) + except WorkbookError as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error getting workbook metadata: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Merge Cells", + destructiveHint=True, + ), +) +def merge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str: + """Merge a range of cells.""" + try: + full_path = get_excel_path(filepath) + result = merge_range(full_path, sheet_name, start_cell, end_cell) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error merging cells: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Unmerge Cells", + destructiveHint=True, + ), +) +def unmerge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str: + """Unmerge a range of cells.""" + try: + full_path = get_excel_path(filepath) + result = unmerge_range(full_path, sheet_name, start_cell, end_cell) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error unmerging cells: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Get Merged Cells", + readOnlyHint=True, + ), +) +def get_merged_cells(filepath: str, sheet_name: str) -> str: + """Get merged cells in a worksheet.""" + try: + full_path = get_excel_path(filepath) + return str(get_merged_ranges(full_path, sheet_name)) + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error getting merged cells: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Copy Range", + destructiveHint=True, + ), +) +def copy_range( + filepath: str, + sheet_name: str, + source_start: str, + source_end: str, + target_start: str, + target_sheet: Optional[str] = None +) -> str: + """Copy a range of cells to another location.""" + try: + full_path = get_excel_path(filepath) + from excel_mcp.sheet import copy_range_operation + result = copy_range_operation( + full_path, + sheet_name, + source_start, + source_end, + target_start, + target_sheet or sheet_name # Use source sheet if target_sheet is None + ) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error copying range: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Delete Range", + destructiveHint=True, + ), +) +def delete_range( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: str, + shift_direction: str = "up" +) -> str: + """Delete a range of cells and shift remaining cells.""" + try: + full_path = get_excel_path(filepath) + from excel_mcp.sheet import delete_range_operation + result = delete_range_operation( + full_path, + sheet_name, + start_cell, + end_cell, + shift_direction + ) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error deleting range: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Validate Excel Range", + readOnlyHint=True, + ), +) +def validate_excel_range( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: Optional[str] = None +) -> str: + """Validate if a range exists and is properly formatted.""" + try: + full_path = get_excel_path(filepath) + range_str = start_cell if not end_cell else f"{start_cell}:{end_cell}" + result = validate_range_impl(full_path, sheet_name, range_str) + return result["message"] + except ValidationError as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error validating range: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Get Data Validation Info", + readOnlyHint=True, + ), +) +def get_data_validation_info( + filepath: str, + sheet_name: str +) -> str: + """ + Get all data validation rules in a worksheet. + + This tool helps identify which cell ranges have validation rules + and what types of validation are applied. + + Args: + filepath: Path to Excel file + sheet_name: Name of worksheet + + Returns: + JSON string containing all validation rules in the worksheet + """ + try: + full_path = get_excel_path(filepath) + from openpyxl import load_workbook + from excel_mcp.cell_validation import get_all_validation_ranges + + wb = load_workbook(full_path, read_only=False) + if sheet_name not in wb.sheetnames: + return f"Error: Sheet '{sheet_name}' not found" + + ws = wb[sheet_name] + validations = get_all_validation_ranges(ws) + wb.close() + + if not validations: + return "No data validation rules found in this worksheet" + + import json + return json.dumps({ + "sheet_name": sheet_name, + "validation_rules": validations + }, indent=2, default=str) + + except Exception as e: + logger.error(f"Error getting validation info: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Insert Rows", + destructiveHint=True, + ), +) +def insert_rows( + filepath: str, + sheet_name: str, + start_row: int, + count: int = 1 +) -> str: + """Insert one or more rows starting at the specified row.""" + try: + full_path = get_excel_path(filepath) + result = insert_row(full_path, sheet_name, start_row, count) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error inserting rows: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Insert Columns", + destructiveHint=True, + ), +) +def insert_columns( + filepath: str, + sheet_name: str, + start_col: int, + count: int = 1 +) -> str: + """Insert one or more columns starting at the specified column.""" + try: + full_path = get_excel_path(filepath) + result = insert_cols(full_path, sheet_name, start_col, count) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error inserting columns: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Delete Rows", + destructiveHint=True, + ), +) +def delete_sheet_rows( + filepath: str, + sheet_name: str, + start_row: int, + count: int = 1 +) -> str: + """Delete one or more rows starting at the specified row.""" + try: + full_path = get_excel_path(filepath) + result = delete_rows(full_path, sheet_name, start_row, count) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error deleting rows: {e}") + raise + +@mcp.tool( + annotations=ToolAnnotations( + title="Delete Columns", + destructiveHint=True, + ), +) +def delete_sheet_columns( + filepath: str, + sheet_name: str, + start_col: int, + count: int = 1 +) -> str: + """Delete one or more columns starting at the specified column.""" + try: + full_path = get_excel_path(filepath) + result = delete_cols(full_path, sheet_name, start_col, count) + return result["message"] + except (ValidationError, SheetError) as e: + return f"Error: {str(e)}" + except Exception as e: + logger.error(f"Error deleting columns: {e}") + raise + +def run_sse(): + """Run Excel MCP server in SSE mode.""" + # Assign value to EXCEL_FILES_PATH in SSE mode + global EXCEL_FILES_PATH + EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files") + # Create directory if it doesn't exist + os.makedirs(EXCEL_FILES_PATH, exist_ok=True) + + try: + logger.info(f"Starting Excel MCP server with SSE transport (files directory: {EXCEL_FILES_PATH})") + mcp.run(transport="sse") + except KeyboardInterrupt: + logger.info("Server stopped by user") + except Exception as e: + logger.error(f"Server failed: {e}") + raise + finally: + logger.info("Server shutdown complete") + +def run_streamable_http(): + """Run Excel MCP server in streamable HTTP mode.""" + # Assign value to EXCEL_FILES_PATH in streamable HTTP mode + global EXCEL_FILES_PATH + EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files") + # Create directory if it doesn't exist + os.makedirs(EXCEL_FILES_PATH, exist_ok=True) + + try: + logger.info(f"Starting Excel MCP server with streamable HTTP transport (files directory: {EXCEL_FILES_PATH})") + mcp.run(transport="streamable-http") + except KeyboardInterrupt: + logger.info("Server stopped by user") + except Exception as e: + logger.error(f"Server failed: {e}") + raise + finally: + logger.info("Server shutdown complete") + +def run_stdio(): + """Run Excel MCP server in stdio mode.""" + # No need to assign EXCEL_FILES_PATH in stdio mode + + try: + logger.info("Starting Excel MCP server with stdio transport") + mcp.run(transport="stdio") + except KeyboardInterrupt: + logger.info("Server stopped by user") + except Exception as e: + logger.error(f"Server failed: {e}") + raise + finally: + logger.info("Server shutdown complete") \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/sheet.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/sheet.py new file mode 100644 index 00000000..357a32dd --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/sheet.py @@ -0,0 +1,475 @@ +import logging +from typing import Any, Dict, Optional +from copy import copy + +from openpyxl import load_workbook +from openpyxl.worksheet.worksheet import Worksheet +from openpyxl.utils import get_column_letter, column_index_from_string +from openpyxl.styles import Font, Border, PatternFill, Side + +from .cell_utils import parse_cell_range +from .exceptions import SheetError, ValidationError + +logger = logging.getLogger(__name__) + +def copy_sheet(filepath: str, source_sheet: str, target_sheet: str) -> Dict[str, Any]: + """Copy a worksheet within the same workbook.""" + try: + wb = load_workbook(filepath) + if source_sheet not in wb.sheetnames: + raise SheetError(f"Source sheet '{source_sheet}' not found") + + if target_sheet in wb.sheetnames: + raise SheetError(f"Target sheet '{target_sheet}' already exists") + + source = wb[source_sheet] + target = wb.copy_worksheet(source) + target.title = target_sheet + + wb.save(filepath) + return {"message": f"Sheet '{source_sheet}' copied to '{target_sheet}'"} + except SheetError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to copy sheet: {e}") + raise SheetError(str(e)) + +def delete_sheet(filepath: str, sheet_name: str) -> Dict[str, Any]: + """Delete a worksheet from the workbook.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + if len(wb.sheetnames) == 1: + raise SheetError("Cannot delete the only sheet in workbook") + + del wb[sheet_name] + wb.save(filepath) + return {"message": f"Sheet '{sheet_name}' deleted"} + except SheetError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to delete sheet: {e}") + raise SheetError(str(e)) + +def rename_sheet(filepath: str, old_name: str, new_name: str) -> Dict[str, Any]: + """Rename a worksheet.""" + try: + wb = load_workbook(filepath) + if old_name not in wb.sheetnames: + raise SheetError(f"Sheet '{old_name}' not found") + + if new_name in wb.sheetnames: + raise SheetError(f"Sheet '{new_name}' already exists") + + sheet = wb[old_name] + sheet.title = new_name + wb.save(filepath) + return {"message": f"Sheet renamed from '{old_name}' to '{new_name}'"} + except SheetError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to rename sheet: {e}") + raise SheetError(str(e)) + +def format_range_string(start_row: int, start_col: int, end_row: int, end_col: int) -> str: + """Format range string from row and column indices.""" + return f"{get_column_letter(start_col)}{start_row}:{get_column_letter(end_col)}{end_row}" + +def copy_range( + source_ws: Worksheet, + target_ws: Worksheet, + source_range: str, + target_start: Optional[str] = None, +) -> None: + """Copy range from source worksheet to target worksheet.""" + # Parse source range + if ':' in source_range: + source_start, source_end = source_range.split(':') + else: + source_start = source_range + source_end = None + + src_start_row, src_start_col, src_end_row, src_end_col = parse_cell_range( + source_start, source_end + ) + + if src_end_row is None: + src_end_row = src_start_row + src_end_col = src_start_col + + if target_start is None: + target_start = source_start + + tgt_start_row, tgt_start_col, _, _ = parse_cell_range(target_start) + + for i, row in enumerate(range(src_start_row, src_end_row + 1)): + for j, col in enumerate(range(src_start_col, src_end_col + 1)): + source_cell = source_ws.cell(row=row, column=col) + target_cell = target_ws.cell(row=tgt_start_row + i, column=tgt_start_col + j) + + target_cell.value = source_cell.value + + try: + # Copy font + font_kwargs = {} + if hasattr(source_cell.font, 'name'): + font_kwargs['name'] = source_cell.font.name + if hasattr(source_cell.font, 'size'): + font_kwargs['size'] = source_cell.font.size + if hasattr(source_cell.font, 'bold'): + font_kwargs['bold'] = source_cell.font.bold + if hasattr(source_cell.font, 'italic'): + font_kwargs['italic'] = source_cell.font.italic + if hasattr(source_cell.font, 'color'): + font_color = None + if source_cell.font.color: + font_color = source_cell.font.color.rgb + font_kwargs['color'] = font_color + target_cell.font = Font(**font_kwargs) + + # Copy border + new_border = Border() + for side in ['left', 'right', 'top', 'bottom']: + source_side = getattr(source_cell.border, side) + if source_side and source_side.style: + side_color = source_side.color.rgb if source_side.color else None + setattr(new_border, side, Side( + style=source_side.style, + color=side_color + )) + target_cell.border = new_border + + # Copy fill + if hasattr(source_cell, 'fill'): + fill_kwargs = {'patternType': source_cell.fill.patternType} + if hasattr(source_cell.fill, 'fgColor') and source_cell.fill.fgColor: + fg_color = None + if hasattr(source_cell.fill.fgColor, 'rgb'): + fg_color = source_cell.fill.fgColor.rgb + fill_kwargs['fgColor'] = fg_color + if hasattr(source_cell.fill, 'bgColor') and source_cell.fill.bgColor: + bg_color = None + if hasattr(source_cell.fill.bgColor, 'rgb'): + bg_color = source_cell.fill.bgColor.rgb + fill_kwargs['bgColor'] = bg_color + target_cell.fill = PatternFill(**fill_kwargs) + + # Copy number format and alignment + if source_cell.number_format: + target_cell.number_format = source_cell.number_format + if source_cell.alignment: + target_cell.alignment = source_cell.alignment + + except Exception: + continue + +def delete_range(worksheet: Worksheet, start_cell: str, end_cell: Optional[str] = None) -> None: + """Delete contents and formatting of a range.""" + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + + if end_row is None: + end_row = start_row + end_col = start_col + + for row in range(start_row, end_row + 1): + for col in range(start_col, end_col + 1): + cell = worksheet.cell(row=row, column=col) + cell.value = None + cell.font = Font() + cell.border = Border() + cell.fill = PatternFill() + cell.number_format = "General" + cell.alignment = None + +def merge_range(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]: + """Merge a range of cells.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + + if end_row is None or end_col is None: + raise SheetError("Both start and end cells must be specified for merging") + + range_string = format_range_string(start_row, start_col, end_row, end_col) + worksheet = wb[sheet_name] + worksheet.merge_cells(range_string) + wb.save(filepath) + return {"message": f"Range '{range_string}' merged in sheet '{sheet_name}'"} + except SheetError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to merge range: {e}") + raise SheetError(str(e)) + +def unmerge_range(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]: + """Unmerge a range of cells.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + + if end_row is None or end_col is None: + raise SheetError("Both start and end cells must be specified for unmerging") + + range_string = format_range_string(start_row, start_col, end_row, end_col) + + # Check if range is actually merged + merged_ranges = worksheet.merged_cells.ranges + target_range = range_string.upper() + + if not any(str(merged_range).upper() == target_range for merged_range in merged_ranges): + raise SheetError(f"Range '{range_string}' is not merged") + + worksheet.unmerge_cells(range_string) + wb.save(filepath) + return {"message": f"Range '{range_string}' unmerged successfully"} + except SheetError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to unmerge range: {e}") + raise SheetError(str(e)) + +def get_merged_ranges(filepath: str, sheet_name: str) -> list[str]: + """Get merged cells in a worksheet.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + worksheet = wb[sheet_name] + return [str(merged_range) for merged_range in worksheet.merged_cells.ranges] + except SheetError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to get merged cells: {e}") + raise SheetError(str(e)) + +def copy_range_operation( + filepath: str, + sheet_name: str, + source_start: str, + source_end: str, + target_start: str, + target_sheet: Optional[str] = None +) -> Dict: + """Copy a range of cells to another location.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + logger.error(f"Sheet '{sheet_name}' not found") + raise ValidationError(f"Sheet '{sheet_name}' not found") + + source_ws = wb[sheet_name] + target_ws = wb[target_sheet] if target_sheet else source_ws + + # Parse source range + try: + start_row, start_col, end_row, end_col = parse_cell_range(source_start, source_end) + except ValueError as e: + logger.error(f"Invalid source range: {e}") + raise ValidationError(f"Invalid source range: {str(e)}") + + # Parse target starting point + try: + target_row = int(''.join(filter(str.isdigit, target_start))) + target_col = column_index_from_string(''.join(filter(str.isalpha, target_start))) + except ValueError as e: + logger.error(f"Invalid target cell: {e}") + raise ValidationError(f"Invalid target cell: {str(e)}") + + # Copy the range + row_offset = target_row - start_row + col_offset = target_col - start_col + + for i in range(start_row, end_row + 1): + for j in range(start_col, end_col + 1): + source_cell = source_ws.cell(row=i, column=j) + target_cell = target_ws.cell(row=i + row_offset, column=j + col_offset) + target_cell.value = source_cell.value + if source_cell.has_style: + target_cell._style = copy(source_cell._style) + + wb.save(filepath) + return {"message": f"Range copied successfully"} + + except (ValidationError, SheetError): + raise + except Exception as e: + logger.error(f"Failed to copy range: {e}") + raise SheetError(f"Failed to copy range: {str(e)}") + +def delete_range_operation( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: Optional[str] = None, + shift_direction: str = "up" +) -> Dict[str, Any]: + """Delete a range of cells and shift remaining cells.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + # Validate range + try: + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + if end_row and end_row > worksheet.max_row: + raise SheetError(f"End row {end_row} out of bounds (1-{worksheet.max_row})") + if end_col and end_col > worksheet.max_column: + raise SheetError(f"End column {end_col} out of bounds (1-{worksheet.max_column})") + except ValueError as e: + raise SheetError(f"Invalid range: {str(e)}") + + # Validate shift direction + if shift_direction not in ["up", "left"]: + raise ValidationError(f"Invalid shift direction: {shift_direction}. Must be 'up' or 'left'") + + range_string = format_range_string( + start_row, start_col, + end_row or start_row, + end_col or start_col + ) + + # Delete range contents + delete_range(worksheet, start_cell, end_cell) + + # Shift cells if needed + if shift_direction == "up": + worksheet.delete_rows(start_row, (end_row or start_row) - start_row + 1) + elif shift_direction == "left": + worksheet.delete_cols(start_col, (end_col or start_col) - start_col + 1) + + wb.save(filepath) + + return {"message": f"Range {range_string} deleted successfully"} + except (ValidationError, SheetError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to delete range: {e}") + raise SheetError(str(e)) + +def insert_row(filepath: str, sheet_name: str, start_row: int, count: int = 1) -> Dict[str, Any]: + """Insert one or more rows starting at the specified row.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + # Validate parameters + if start_row < 1: + raise ValidationError("Start row must be 1 or greater") + if count < 1: + raise ValidationError("Count must be 1 or greater") + + worksheet.insert_rows(start_row, count) + wb.save(filepath) + + return {"message": f"Inserted {count} row(s) starting at row {start_row} in sheet '{sheet_name}'"} + except (ValidationError, SheetError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to insert rows: {e}") + raise SheetError(str(e)) + +def insert_cols(filepath: str, sheet_name: str, start_col: int, count: int = 1) -> Dict[str, Any]: + """Insert one or more columns starting at the specified column.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + # Validate parameters + if start_col < 1: + raise ValidationError("Start column must be 1 or greater") + if count < 1: + raise ValidationError("Count must be 1 or greater") + + worksheet.insert_cols(start_col, count) + wb.save(filepath) + + return {"message": f"Inserted {count} column(s) starting at column {start_col} in sheet '{sheet_name}'"} + except (ValidationError, SheetError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to insert columns: {e}") + raise SheetError(str(e)) + +def delete_rows(filepath: str, sheet_name: str, start_row: int, count: int = 1) -> Dict[str, Any]: + """Delete one or more rows starting at the specified row.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + # Validate parameters + if start_row < 1: + raise ValidationError("Start row must be 1 or greater") + if count < 1: + raise ValidationError("Count must be 1 or greater") + if start_row > worksheet.max_row: + raise ValidationError(f"Start row {start_row} exceeds worksheet bounds (max row: {worksheet.max_row})") + + worksheet.delete_rows(start_row, count) + wb.save(filepath) + + return {"message": f"Deleted {count} row(s) starting at row {start_row} in sheet '{sheet_name}'"} + except (ValidationError, SheetError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to delete rows: {e}") + raise SheetError(str(e)) + +def delete_cols(filepath: str, sheet_name: str, start_col: int, count: int = 1) -> Dict[str, Any]: + """Delete one or more columns starting at the specified column.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise SheetError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + # Validate parameters + if start_col < 1: + raise ValidationError("Start column must be 1 or greater") + if count < 1: + raise ValidationError("Count must be 1 or greater") + if start_col > worksheet.max_column: + raise ValidationError(f"Start column {start_col} exceeds worksheet bounds (max column: {worksheet.max_column})") + + worksheet.delete_cols(start_col, count) + wb.save(filepath) + + return {"message": f"Deleted {count} column(s) starting at column {start_col} in sheet '{sheet_name}'"} + except (ValidationError, SheetError) as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to delete columns: {e}") + raise SheetError(str(e)) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/tables.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/tables.py new file mode 100644 index 00000000..ed5168d4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/tables.py @@ -0,0 +1,69 @@ +import uuid +import logging + +from openpyxl import load_workbook +from openpyxl.worksheet.table import Table, TableStyleInfo +from .exceptions import DataError + +logger = logging.getLogger(__name__) + +def create_excel_table( + filepath: str, + sheet_name: str, + data_range: str, + table_name: str | None = None, + table_style: str = "TableStyleMedium9" +) -> dict: + """Creates a native Excel table for the given data range. + + Args: + filepath: Path to the Excel file. + sheet_name: Name of the worksheet. + data_range: The cell range for the table (e.g., "A1:D5"). + table_name: A unique name for the table. If not provided, a unique name is generated. + table_style: The visual style to apply to the table. + + Returns: + A dictionary with a success message and table details. + """ + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise DataError(f"Sheet '{sheet_name}' not found.") + + ws = wb[sheet_name] + + # If no table name is provided, generate a unique one + if not table_name: + table_name = f"Table_{uuid.uuid4().hex[:8]}" + + # Check if table name already exists + if table_name in ws.parent.defined_names: + raise DataError(f"Table name '{table_name}' already exists.") + + # Create the table + table = Table(displayName=table_name, ref=data_range) + + # Apply style + style = TableStyleInfo( + name=table_style, + showFirstColumn=False, + showLastColumn=False, + showRowStripes=True, + showColumnStripes=False + ) + table.tableStyleInfo = style + + ws.add_table(table) + + wb.save(filepath) + + return { + "message": f"Successfully created table '{table_name}' in sheet '{sheet_name}'.", + "table_name": table_name, + "range": data_range + } + + except Exception as e: + logger.error(f"Failed to create table: {e}") + raise DataError(str(e)) \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/validation.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/validation.py new file mode 100644 index 00000000..faaa838c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/validation.py @@ -0,0 +1,235 @@ +import logging +import re +from typing import Any + +from openpyxl import load_workbook +from openpyxl.utils import get_column_letter +from openpyxl.worksheet.worksheet import Worksheet + +from .cell_utils import parse_cell_range, validate_cell_reference +from .exceptions import ValidationError + +logger = logging.getLogger(__name__) + +def validate_formula_in_cell_operation( + filepath: str, + sheet_name: str, + cell: str, + formula: str +) -> dict[str, Any]: + """Validate Excel formula before writing""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise ValidationError(f"Sheet '{sheet_name}' not found") + + if not validate_cell_reference(cell): + raise ValidationError(f"Invalid cell reference: {cell}") + + # First validate the provided formula's syntax + is_valid, message = validate_formula(formula) + if not is_valid: + raise ValidationError(f"Invalid formula syntax: {message}") + + # Additional validation for cell references in formula + cell_refs = re.findall(r'[A-Z]+[0-9]+(?::[A-Z]+[0-9]+)?', formula) + for ref in cell_refs: + if ':' in ref: # Range reference + start, end = ref.split(':') + if not (validate_cell_reference(start) and validate_cell_reference(end)): + raise ValidationError(f"Invalid cell range reference in formula: {ref}") + else: # Single cell reference + if not validate_cell_reference(ref): + raise ValidationError(f"Invalid cell reference in formula: {ref}") + + # Now check if there's a formula in the cell and compare + sheet = wb[sheet_name] + cell_obj = sheet[cell] + current_formula = cell_obj.value + + # If cell has a formula (starts with =) + if isinstance(current_formula, str) and current_formula.startswith('='): + if formula.startswith('='): + if current_formula != formula: + return { + "message": "Formula is valid but doesn't match cell content", + "valid": True, + "matches": False, + "cell": cell, + "provided_formula": formula, + "current_formula": current_formula + } + else: + if current_formula != f"={formula}": + return { + "message": "Formula is valid but doesn't match cell content", + "valid": True, + "matches": False, + "cell": cell, + "provided_formula": formula, + "current_formula": current_formula + } + else: + return { + "message": "Formula is valid and matches cell content", + "valid": True, + "matches": True, + "cell": cell, + "formula": formula + } + else: + return { + "message": "Formula is valid but cell contains no formula", + "valid": True, + "matches": False, + "cell": cell, + "provided_formula": formula, + "current_content": str(current_formula) if current_formula else "" + } + + except ValidationError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to validate formula: {e}") + raise ValidationError(str(e)) + +def validate_range_in_sheet_operation( + filepath: str, + sheet_name: str, + start_cell: str, + end_cell: str | None = None, +) -> dict[str, Any]: + """Validate if a range exists in a worksheet and return data range info.""" + try: + wb = load_workbook(filepath) + if sheet_name not in wb.sheetnames: + raise ValidationError(f"Sheet '{sheet_name}' not found") + + worksheet = wb[sheet_name] + + # Get actual data dimensions + data_max_row = worksheet.max_row + data_max_col = worksheet.max_column + + # Validate range + try: + start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell) + except ValueError as e: + raise ValidationError(f"Invalid range: {str(e)}") + + # If end not specified, use start + if end_row is None: + end_row = start_row + if end_col is None: + end_col = start_col + + # Validate bounds against maximum possible Excel limits + is_valid, message = validate_range_bounds( + worksheet, start_row, start_col, end_row, end_col + ) + if not is_valid: + raise ValidationError(message) + + range_str = f"{start_cell}" if end_cell is None else f"{start_cell}:{end_cell}" + data_range_str = f"A1:{get_column_letter(data_max_col)}{data_max_row}" + + # Check if range is within data or extends beyond + extends_beyond_data = ( + end_row > data_max_row or + end_col > data_max_col + ) + + return { + "message": ( + f"Range '{range_str}' is valid. " + f"Sheet contains data in range '{data_range_str}'" + ), + "valid": True, + "range": range_str, + "data_range": data_range_str, + "extends_beyond_data": extends_beyond_data, + "data_dimensions": { + "max_row": data_max_row, + "max_col": data_max_col, + "max_col_letter": get_column_letter(data_max_col) + } + } + except ValidationError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to validate range: {e}") + raise ValidationError(str(e)) + +def validate_formula(formula: str) -> tuple[bool, str]: + """Validate Excel formula syntax and safety""" + if not formula.startswith("="): + return False, "Formula must start with '='" + + # Remove the '=' prefix for validation + formula = formula[1:] + + # Check for balanced parentheses + parens = 0 + for c in formula: + if c == "(": + parens += 1 + elif c == ")": + parens -= 1 + if parens < 0: + return False, "Unmatched closing parenthesis" + + if parens > 0: + return False, "Unclosed parenthesis" + + # Basic function name validation + func_pattern = r"([A-Z]+)\(" + funcs = re.findall(func_pattern, formula) + unsafe_funcs = {"INDIRECT", "HYPERLINK", "WEBSERVICE", "DGET", "RTD"} + + for func in funcs: + if func in unsafe_funcs: + return False, f"Unsafe function: {func}" + + return True, "Formula is valid" + + +def validate_range_bounds( + worksheet: Worksheet, + start_row: int, + start_col: int, + end_row: int | None = None, + end_col: int | None = None, +) -> tuple[bool, str]: + """Validate that cell range is within worksheet bounds""" + max_row = worksheet.max_row + max_col = worksheet.max_column + + try: + # Check start cell bounds + if start_row < 1 or start_row > max_row: + return False, f"Start row {start_row} out of bounds (1-{max_row})" + if start_col < 1 or start_col > max_col: + return False, ( + f"Start column {get_column_letter(start_col)} " + f"out of bounds (A-{get_column_letter(max_col)})" + ) + + # If end cell specified, check its bounds + if end_row is not None and end_col is not None: + if end_row < start_row: + return False, "End row cannot be before start row" + if end_col < start_col: + return False, "End column cannot be before start column" + if end_row > max_row: + return False, f"End row {end_row} out of bounds (1-{max_row})" + if end_col > max_col: + return False, ( + f"End column {get_column_letter(end_col)} " + f"out of bounds (A-{get_column_letter(max_col)})" + ) + + return True, "Range is valid" + except Exception as e: + return False, f"Invalid range: {e!s}" \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/workbook.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/workbook.py new file mode 100644 index 00000000..9d6f193f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/workbook.py @@ -0,0 +1,96 @@ +import logging +from pathlib import Path +from typing import Any + +from openpyxl import Workbook, load_workbook +from openpyxl.utils import get_column_letter + +from .exceptions import WorkbookError + +logger = logging.getLogger(__name__) + +def create_workbook(filepath: str, sheet_name: str = "Sheet1") -> dict[str, Any]: + """Create a new Excel workbook with optional custom sheet name""" + try: + wb = Workbook() + # Rename default sheet + if "Sheet" in wb.sheetnames: + sheet = wb["Sheet"] + sheet.title = sheet_name + else: + wb.create_sheet(sheet_name) + + path = Path(filepath) + path.parent.mkdir(parents=True, exist_ok=True) + wb.save(str(path)) + return { + "message": f"Created workbook: {filepath}", + "active_sheet": sheet_name, + "workbook": wb + } + except Exception as e: + logger.error(f"Failed to create workbook: {e}") + raise WorkbookError(f"Failed to create workbook: {e!s}") + +def get_or_create_workbook(filepath: str) -> Workbook: + """Get existing workbook or create new one if it doesn't exist""" + try: + return load_workbook(filepath) + except FileNotFoundError: + return create_workbook(filepath)["workbook"] + +def create_sheet(filepath: str, sheet_name: str) -> dict: + """Create a new worksheet in the workbook if it doesn't exist.""" + try: + wb = load_workbook(filepath) + + # Check if sheet already exists + if sheet_name in wb.sheetnames: + raise WorkbookError(f"Sheet {sheet_name} already exists") + + # Create new sheet + wb.create_sheet(sheet_name) + wb.save(filepath) + wb.close() + return {"message": f"Sheet {sheet_name} created successfully"} + except WorkbookError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to create sheet: {e}") + raise WorkbookError(str(e)) + +def get_workbook_info(filepath: str, include_ranges: bool = False) -> dict[str, Any]: + """Get metadata about workbook including sheets, ranges, etc.""" + try: + path = Path(filepath) + if not path.exists(): + raise WorkbookError(f"File not found: {filepath}") + + wb = load_workbook(filepath, read_only=False) + + info = { + "filename": path.name, + "sheets": wb.sheetnames, + "size": path.stat().st_size, + "modified": path.stat().st_mtime + } + + if include_ranges: + # Add used ranges for each sheet + ranges = {} + for sheet_name in wb.sheetnames: + ws = wb[sheet_name] + if ws.max_row > 0 and ws.max_column > 0: + ranges[sheet_name] = f"A1:{get_column_letter(ws.max_column)}{ws.max_row}" + info["used_ranges"] = ranges + + wb.close() + return info + + except WorkbookError as e: + logger.error(str(e)) + raise + except Exception as e: + logger.error(f"Failed to get workbook info: {e}") + raise WorkbookError(str(e)) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/uv.lock new file mode 100644 index 00000000..691e4b43 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/uv.lock @@ -0,0 +1,1211 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "authlib" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +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 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "45.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092 }, + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926 }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235 }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785 }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050 }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379 }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355 }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087 }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873 }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651 }, + { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050 }, + { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224 }, + { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143 }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780 }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091 }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711 }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299 }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558 }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020 }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759 }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991 }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189 }, + { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769 }, + { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016 }, + { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762 }, + { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906 }, + { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411 }, + { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942 }, + { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079 }, + { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362 }, + { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878 }, + { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447 }, + { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778 }, + { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627 }, + { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593 }, + { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106 }, +] + +[[package]] +name = "cyclopts" +version = "3.22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4.0'" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/d5/24c6c894f3833bc93d4944c2064309dfd633c0becf93e16fc79d76edd388/cyclopts-3.22.5.tar.gz", hash = "sha256:fa2450b9840abc41c6aa37af5eaeafc7a1264e08054e3a2fe39d49aa154f592a", size = 74890 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/e5/a7b6db64f08cfe065e531ec6b508fa7dac704fab70d05adb5bc0c2c1d1b6/cyclopts-3.22.5-py3-none-any.whl", hash = "sha256:92efb4a094d9812718d7efe0bffa319a19cb661f230dbf24406c18cd8809fb82", size = 84994 }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, +] + +[[package]] +name = "docutils" +version = "0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709 }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, +] + +[[package]] +name = "excel-mcp-server" +version = "0.1.7" +source = { editable = "." } +dependencies = [ + { name = "fastmcp" }, + { name = "mcp", extra = ["cli"] }, + { name = "openpyxl" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastmcp", specifier = ">=2.0.0,<3.0.0" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.10.1" }, + { name = "openpyxl", specifier = ">=3.1.5" }, + { name = "typer", specifier = ">=0.16.0" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "fastmcp" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "openapi-core" }, + { name = "openapi-pydantic" }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/02/0701624e938fe4d1f13464de9bdc27be9aba2e4c4d41edab3ea496d31751/fastmcp-2.11.0.tar.gz", hash = "sha256:af0c52988607d8e9197df300e91880169e8fe24fd6ca177dca6a9eb6b245ce3c", size = 2663877 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/9a/51108b68e77650a7289b5f1ceff8dc0929ab48a26d1d2015f22121a9d183/fastmcp-2.11.0-py3-none-any.whl", hash = "sha256:8709a04522e66fda407b469fbe4d3290651aa7b06097b91c097e9a973c9b9bb3", size = 256193 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { 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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709 }, +] + +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/1f56571ed82fb324f293661690635cf42c41deb8a70a6c9e6edc3e9bb3c8/lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c", size = 44736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c8/457f1555f066f5bacc44337141294153dc993b5e9132272ab54a64ee98a2/lazy_object_proxy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:132bc8a34f2f2d662a851acfd1b93df769992ed1b81e2b1fda7db3e73b0d5a18", size = 28045 }, + { url = "https://files.pythonhosted.org/packages/18/33/3260b4f8de6f0942008479fee6950b2b40af11fc37dba23aa3672b0ce8a6/lazy_object_proxy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:01261a3afd8621a1accb5682df2593dc7ec7d21d38f411011a5712dcd418fbed", size = 28441 }, + { url = "https://files.pythonhosted.org/packages/51/f6/eb645ca1ff7408bb69e9b1fe692cce1d74394efdbb40d6207096c0cd8381/lazy_object_proxy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:090935756cc041e191f22f4f9c7fd4fe9a454717067adf5b1bbd2ce3046b556e", size = 28047 }, + { url = "https://files.pythonhosted.org/packages/13/9c/aabbe1e8b99b8b0edb846b49a517edd636355ac97364419d9ba05b8fa19f/lazy_object_proxy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:76ec715017f06410f57df442c1a8d66e6b5f7035077785b129817f5ae58810a4", size = 28440 }, + { url = "https://files.pythonhosted.org/packages/4d/24/dae4759469e9cd318fef145f7cfac7318261b47b23a4701aa477b0c3b42c/lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b", size = 28142 }, + { url = "https://files.pythonhosted.org/packages/de/0c/645a881f5f27952a02f24584d96f9f326748be06ded2cee25f8f8d1cd196/lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3", size = 28380 }, + { url = "https://files.pythonhosted.org/packages/a8/0f/6e004f928f7ff5abae2b8e1f68835a3870252f886e006267702e1efc5c7b/lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd", size = 28149 }, + { url = "https://files.pythonhosted.org/packages/63/cb/b8363110e32cc1fd82dc91296315f775d37a39df1c1cfa976ec1803dac89/lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7", size = 28389 }, + { url = "https://files.pythonhosted.org/packages/7b/89/68c50fcfd81e11480cd8ee7f654c9bd790a9053b9a0efe9983d46106f6a9/lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3", size = 28777 }, + { url = "https://files.pythonhosted.org/packages/39/d0/7e967689e24de8ea6368ec33295f9abc94b9f3f0cd4571bfe148dc432190/lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8", size = 29598 }, + { url = "https://files.pythonhosted.org/packages/e7/1e/fb441c07b6662ec1fc92b249225ba6e6e5221b05623cb0131d082f782edc/lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b", size = 16635 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mcp" +version = "1.10.1" +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 = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/68/63045305f29ff680a9cd5be360c755270109e6b76f696ea6824547ddbc30/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2", size = 392969 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/3f/435a5b3d10ae242a9d6c2b33175551173c3c61fe637dc893be05c4ed0aaf/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5", size = 150878 }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.7.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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 }, +] + +[[package]] +name = "openapi-core" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "more-itertools" }, + { name = "openapi-schema-validator" }, + { name = "openapi-spec-validator" }, + { name = "parse" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595 }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 }, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755 }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713 }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, +] + +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126 }, +] + +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { 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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyperclip" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961 } + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { 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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rich-rst" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621 }, +] + +[[package]] +name = "rpds-py" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466 }, + { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530 }, + { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933 }, + { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973 }, + { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293 }, + { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787 }, + { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312 }, + { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403 }, + { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323 }, + { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541 }, + { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442 }, + { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314 }, + { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610 }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032 }, + { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525 }, + { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089 }, + { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255 }, + { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283 }, + { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881 }, + { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822 }, + { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347 }, + { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956 }, + { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363 }, + { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123 }, + { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732 }, + { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917 }, + { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933 }, + { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447 }, + { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711 }, + { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865 }, + { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763 }, + { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651 }, + { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079 }, + { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379 }, + { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033 }, + { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639 }, + { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105 }, + { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272 }, + { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995 }, + { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198 }, + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917 }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073 }, + { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214 }, + { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113 }, + { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189 }, + { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998 }, + { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903 }, + { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785 }, + { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329 }, + { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875 }, + { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636 }, + { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663 }, + { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428 }, + { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571 }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475 }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692 }, + { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415 }, + { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783 }, + { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844 }, + { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105 }, + { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440 }, + { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759 }, + { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032 }, + { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416 }, + { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049 }, + { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428 }, + { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524 }, + { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292 }, + { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334 }, + { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875 }, + { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993 }, + { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683 }, + { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825 }, + { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292 }, + { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435 }, + { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410 }, + { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724 }, + { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285 }, + { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459 }, + { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083 }, + { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291 }, + { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445 }, + { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206 }, + { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330 }, + { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254 }, + { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094 }, + { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889 }, + { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301 }, + { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891 }, + { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044 }, + { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774 }, + { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886 }, + { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027 }, + { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821 }, + { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226 }, + { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230 }, + { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363 }, + { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146 }, + { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804 }, + { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820 }, + { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567 }, + { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520 }, + { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362 }, + { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113 }, + { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429 }, + { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950 }, + { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505 }, + { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468 }, + { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680 }, + { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035 }, + { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922 }, + { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822 }, + { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336 }, + { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871 }, + { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439 }, + { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380 }, + { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sse-starlette" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, +] + +[[package]] +name = "starlette" +version = "0.45.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, +] + +[[package]] +name = "werkzeug" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371 }, +] diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/Dockerfile new file mode 100644 index 00000000..418b1400 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22.12-alpine AS builder + +WORKDIR /app + +COPY src/filesystem /app +COPY tsconfig.json /tsconfig.json + +RUN --mount=type=cache,target=/root/.npm npm install + +RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev + + +FROM node:22-alpine AS release + +WORKDIR /app + +COPY --from=builder /app/dist /app/dist +COPY --from=builder /app/package.json /app/package.json +COPY --from=builder /app/package-lock.json /app/package-lock.json + +ENV NODE_ENV=production + +RUN npm ci --ignore-scripts --omit-dev + +ENTRYPOINT ["node", "/app/dist/index.js"] \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/README.md new file mode 100644 index 00000000..bf087a2b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/README.md @@ -0,0 +1,321 @@ +# Filesystem MCP Server + +Node.js server implementing Model Context Protocol (MCP) for filesystem operations. + +## Features + +- Read/write files +- Create/list/delete directories +- Move files/directories +- Search files +- Get file metadata +- Dynamic directory access control via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) + +## Directory Access Control + +The server uses a flexible directory access control system. Directories can be specified via command-line arguments or dynamically via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots). + +### Method 1: Command-line Arguments +Specify Allowed directories when starting the server: +```bash +mcp-server-filesystem /path/to/dir1 /path/to/dir2 +``` + +### Method 2: MCP Roots (Recommended) +MCP clients that support [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) can dynamically update the Allowed directories. + +Roots notified by Client to Server, completely replace any server-side Allowed directories when provided. + +**Important**: If server starts without command-line arguments AND client doesn't support roots protocol (or provides empty roots), the server will throw an error during initialization. + +This is the recommended method, as this enables runtime directory updates via `roots/list_changed` notifications without server restart, providing a more flexible and modern integration experience. + +### How It Works + +The server's directory access control follows this flow: + +1. **Server Startup** + - Server starts with directories from command-line arguments (if provided) + - If no arguments provided, server starts with empty allowed directories + +2. **Client Connection & Initialization** + - Client connects and sends `initialize` request with capabilities + - Server checks if client supports roots protocol (`capabilities.roots`) + +3. **Roots Protocol Handling** (if client supports roots) + - **On initialization**: Server requests roots from client via `roots/list` + - Client responds with its configured roots + - Server replaces ALL allowed directories with client's roots + - **On runtime updates**: Client can send `notifications/roots/list_changed` + - Server requests updated roots and replaces allowed directories again + +4. **Fallback Behavior** (if client doesn't support roots) + - Server continues using command-line directories only + - No dynamic updates possible + +5. **Access Control** + - All filesystem operations are restricted to allowed directories + - Use `list_allowed_directories` tool to see current directories + - Server requires at least ONE allowed directory to operate + +**Note**: The server will only allow operations within directories specified either via `args` or via Roots. + + + +## API + +### Tools + +- **read_text_file** + - Read complete contents of a file as text + - Inputs: + - `path` (string) + - `head` (number, optional): First N lines + - `tail` (number, optional): Last N lines + - Always treats the file as UTF-8 text regardless of extension + - Cannot specify both `head` and `tail` simultaneously + +- **read_media_file** + - Read an image or audio file + - Inputs: + - `path` (string) + - Streams the file and returns base64 data with the corresponding MIME type + +- **read_multiple_files** + - Read multiple files simultaneously + - Input: `paths` (string[]) + - Failed reads won't stop the entire operation + +- **write_file** + - Create new file or overwrite existing (exercise caution with this) + - Inputs: + - `path` (string): File location + - `content` (string): File content + +- **edit_file** + - Make selective edits using advanced pattern matching and formatting + - Features: + - Line-based and multi-line content matching + - Whitespace normalization with indentation preservation + - Multiple simultaneous edits with correct positioning + - Indentation style detection and preservation + - Git-style diff output with context + - Preview changes with dry run mode + - Inputs: + - `path` (string): File to edit + - `edits` (array): List of edit operations + - `oldText` (string): Text to search for (can be substring) + - `newText` (string): Text to replace with + - `dryRun` (boolean): Preview changes without applying (default: false) + - Returns detailed diff and match information for dry runs, otherwise applies changes + - Best Practice: Always use dryRun first to preview changes before applying them + +- **create_directory** + - Create new directory or ensure it exists + - Input: `path` (string) + - Creates parent directories if needed + - Succeeds silently if directory exists + +- **list_directory** + - List directory contents with [FILE] or [DIR] prefixes + - Input: `path` (string) + +- **list_directory_with_sizes** + - List directory contents with [FILE] or [DIR] prefixes, including file sizes + - Inputs: + - `path` (string): Directory path to list + - `sortBy` (string, optional): Sort entries by "name" or "size" (default: "name") + - Returns detailed listing with file sizes and summary statistics + - Shows total files, directories, and combined size + +- **move_file** + - Move or rename files and directories + - Inputs: + - `source` (string) + - `destination` (string) + - Fails if destination exists + +- **search_files** + - Recursively search for files/directories that match or do not match patterns + - Inputs: + - `path` (string): Starting directory + - `pattern` (string): Search pattern + - `excludePatterns` (string[]): Exclude any patterns. + - Glob-style pattern matching + - Returns full paths to matches + +- **directory_tree** + - Get recursive JSON tree structure of directory contents + - Inputs: + - `path` (string): Starting directory + - `excludePatterns` (string[]): Exclude any patterns. Glob formats are supported. + - Returns: + - JSON array where each entry contains: + - `name` (string): File/directory name + - `type` ('file'|'directory'): Entry type + - `children` (array): Present only for directories + - Empty array for empty directories + - Omitted for files + - Output is formatted with 2-space indentation for readability + +- **get_file_info** + - Get detailed file/directory metadata + - Input: `path` (string) + - Returns: + - Size + - Creation time + - Modified time + - Access time + - Type (file/directory) + - Permissions + +- **list_allowed_directories** + - List all directories the server is allowed to access + - No input required + - Returns: + - Directories that this server can read/write from + +### Tool annotations (MCP hints) + +This server sets [MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#toolannotations) +on each tool so clients can: + +- Distinguish **read‑only** tools from write‑capable tools. +- Understand which write operations are **idempotent** (safe to retry with the same arguments). +- Highlight operations that may be **destructive** (overwriting or heavily mutating data). + +The mapping for filesystem tools is: + +| Tool | readOnlyHint | idempotentHint | destructiveHint | Notes | +|-----------------------------|--------------|----------------|-----------------|--------------------------------------------------| +| `read_text_file` | `true` | – | – | Pure read | +| `read_media_file` | `true` | – | – | Pure read | +| `read_multiple_files` | `true` | – | – | Pure read | +| `list_directory` | `true` | – | – | Pure read | +| `list_directory_with_sizes` | `true` | – | – | Pure read | +| `directory_tree` | `true` | – | – | Pure read | +| `search_files` | `true` | – | – | Pure read | +| `get_file_info` | `true` | – | – | Pure read | +| `list_allowed_directories` | `true` | – | – | Pure read | +| `create_directory` | `false` | `true` | `false` | Re‑creating the same dir is a no‑op | +| `write_file` | `false` | `true` | `true` | Overwrites existing files | +| `edit_file` | `false` | `false` | `true` | Re‑applying edits can fail or double‑apply | +| `move_file` | `false` | `false` | `true` | Deletes source file | + +> Note: `idempotentHint` and `destructiveHint` are meaningful only when `readOnlyHint` is `false`, as defined by the MCP spec. + +## Usage with Claude Desktop +Add this to your `claude_desktop_config.json`: + +Note: you can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server. + +### Docker +Note: all directories must be mounted to `/projects` by default. + +```json +{ + "mcpServers": { + "filesystem": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--mount", "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop", + "--mount", "type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro", + "--mount", "type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt", + "mcp/filesystem", + "/projects" + ] + } + } +} +``` + +### NPX + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/Users/username/Desktop", + "/path/to/other/allowed/dir" + ] + } + } +} +``` + +## Usage with VS Code + +For quick installation, click the installation buttons below... + +[![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D&quality=insiders) + +[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D&quality=insiders) + +For manual installation, you can configure the MCP server using one of these methods: + +**Method 1: User Configuration (Recommended)** +Add the configuration to your user-level MCP configuration file. Open the Command Palette (`Ctrl + Shift + P`) and run `MCP: Open User Configuration`. This will open your user `mcp.json` file where you can add the server configuration. + +**Method 2: Workspace Configuration** +Alternatively, you can add the configuration to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others. + +> For more details about MCP configuration in VS Code, see the [official VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers). + +You can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server. + +### Docker +Note: all directories must be mounted to `/projects` by default. + +```json +{ + "servers": { + "filesystem": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--mount", "type=bind,src=${workspaceFolder},dst=/projects/workspace", + "mcp/filesystem", + "/projects" + ] + } + } +} +``` + +### NPX + +```json +{ + "servers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "${workspaceFolder}" + ] + } + } +} +``` + +## Build + +Docker build: + +```bash +docker build -t mcp/filesystem -f src/filesystem/Dockerfile . +``` + +## License + +This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/directory-tree.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/directory-tree.test.ts new file mode 100644 index 00000000..04c8278c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/directory-tree.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; + +// We need to test the buildTree function, but it's defined inside the request handler +// So we'll extract the core logic into a testable function +import { minimatch } from 'minimatch'; + +interface TreeEntry { + name: string; + type: 'file' | 'directory'; + children?: TreeEntry[]; +} + +async function buildTreeForTesting(currentPath: string, rootPath: string, excludePatterns: string[] = []): Promise { + const entries = await fs.readdir(currentPath, {withFileTypes: true}); + const result: TreeEntry[] = []; + + for (const entry of entries) { + const relativePath = path.relative(rootPath, path.join(currentPath, entry.name)); + const shouldExclude = excludePatterns.some(pattern => { + if (pattern.includes('*')) { + return minimatch(relativePath, pattern, {dot: true}); + } + // For files: match exact name or as part of path + // For directories: match as directory path + return minimatch(relativePath, pattern, {dot: true}) || + minimatch(relativePath, `**/${pattern}`, {dot: true}) || + minimatch(relativePath, `**/${pattern}/**`, {dot: true}); + }); + if (shouldExclude) + continue; + + const entryData: TreeEntry = { + name: entry.name, + type: entry.isDirectory() ? 'directory' : 'file' + }; + + if (entry.isDirectory()) { + const subPath = path.join(currentPath, entry.name); + entryData.children = await buildTreeForTesting(subPath, rootPath, excludePatterns); + } + + result.push(entryData); + } + + return result; +} + +describe('buildTree exclude patterns', () => { + let testDir: string; + + beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'filesystem-test-')); + + // Create test directory structure + await fs.mkdir(path.join(testDir, 'src')); + await fs.mkdir(path.join(testDir, 'node_modules')); + await fs.mkdir(path.join(testDir, '.git')); + await fs.mkdir(path.join(testDir, 'nested', 'node_modules'), { recursive: true }); + + // Create test files + await fs.writeFile(path.join(testDir, '.env'), 'SECRET=value'); + await fs.writeFile(path.join(testDir, '.env.local'), 'LOCAL_SECRET=value'); + await fs.writeFile(path.join(testDir, 'src', 'index.js'), 'console.log("hello");'); + await fs.writeFile(path.join(testDir, 'package.json'), '{}'); + await fs.writeFile(path.join(testDir, 'node_modules', 'module.js'), 'module.exports = {};'); + await fs.writeFile(path.join(testDir, 'nested', 'node_modules', 'deep.js'), 'module.exports = {};'); + }); + + afterEach(async () => { + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('should exclude files matching simple patterns', async () => { + // Test the current implementation - this will fail until the bug is fixed + const tree = await buildTreeForTesting(testDir, testDir, ['.env']); + const fileNames = tree.map(entry => entry.name); + + expect(fileNames).not.toContain('.env'); + expect(fileNames).toContain('.env.local'); // Should not exclude this + expect(fileNames).toContain('src'); + expect(fileNames).toContain('package.json'); + }); + + it('should exclude directories matching simple patterns', async () => { + const tree = await buildTreeForTesting(testDir, testDir, ['node_modules']); + const dirNames = tree.map(entry => entry.name); + + expect(dirNames).not.toContain('node_modules'); + expect(dirNames).toContain('src'); + expect(dirNames).toContain('.git'); + }); + + it('should exclude nested directories with same pattern', async () => { + const tree = await buildTreeForTesting(testDir, testDir, ['node_modules']); + + // Find the nested directory + const nestedDir = tree.find(entry => entry.name === 'nested'); + expect(nestedDir).toBeDefined(); + expect(nestedDir!.children).toBeDefined(); + + // The nested/node_modules should also be excluded + const nestedChildren = nestedDir!.children!.map(child => child.name); + expect(nestedChildren).not.toContain('node_modules'); + }); + + it('should handle glob patterns correctly', async () => { + const tree = await buildTreeForTesting(testDir, testDir, ['*.env']); + const fileNames = tree.map(entry => entry.name); + + expect(fileNames).not.toContain('.env'); + expect(fileNames).toContain('.env.local'); // *.env should not match .env.local + expect(fileNames).toContain('src'); + }); + + it('should handle dot files correctly', async () => { + const tree = await buildTreeForTesting(testDir, testDir, ['.git']); + const dirNames = tree.map(entry => entry.name); + + expect(dirNames).not.toContain('.git'); + expect(dirNames).toContain('.env'); // Should not exclude this + }); + + it('should work with multiple exclude patterns', async () => { + const tree = await buildTreeForTesting(testDir, testDir, ['node_modules', '.env', '.git']); + const entryNames = tree.map(entry => entry.name); + + expect(entryNames).not.toContain('node_modules'); + expect(entryNames).not.toContain('.env'); + expect(entryNames).not.toContain('.git'); + expect(entryNames).toContain('src'); + expect(entryNames).toContain('package.json'); + }); + + it('should handle empty exclude patterns', async () => { + const tree = await buildTreeForTesting(testDir, testDir, []); + const entryNames = tree.map(entry => entry.name); + + // All entries should be included + expect(entryNames).toContain('node_modules'); + expect(entryNames).toContain('.env'); + expect(entryNames).toContain('.git'); + expect(entryNames).toContain('src'); + }); +}); \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/lib.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/lib.test.ts new file mode 100644 index 00000000..f7e585af --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/lib.test.ts @@ -0,0 +1,725 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; +import { + // Pure utility functions + formatSize, + normalizeLineEndings, + createUnifiedDiff, + // Security & validation functions + validatePath, + setAllowedDirectories, + // File operations + getFileStats, + readFileContent, + writeFileContent, + // Search & filtering functions + searchFilesWithValidation, + // File editing functions + applyFileEdits, + tailFile, + headFile +} from '../lib.js'; + +// Mock fs module +vi.mock('fs/promises'); +const mockFs = fs as any; + +describe('Lib Functions', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Set up allowed directories for tests + const allowedDirs = process.platform === 'win32' ? ['C:\\Users\\test', 'C:\\temp', 'C:\\allowed'] : ['/home/user', '/tmp', '/allowed']; + setAllowedDirectories(allowedDirs); + }); + + afterEach(() => { + vi.restoreAllMocks(); + // Clear allowed directories after tests + setAllowedDirectories([]); + }); + + describe('Pure Utility Functions', () => { + describe('formatSize', () => { + it('formats bytes correctly', () => { + expect(formatSize(0)).toBe('0 B'); + expect(formatSize(512)).toBe('512 B'); + expect(formatSize(1024)).toBe('1.00 KB'); + expect(formatSize(1536)).toBe('1.50 KB'); + expect(formatSize(1048576)).toBe('1.00 MB'); + expect(formatSize(1073741824)).toBe('1.00 GB'); + expect(formatSize(1099511627776)).toBe('1.00 TB'); + }); + + it('handles edge cases', () => { + expect(formatSize(1023)).toBe('1023 B'); + expect(formatSize(1025)).toBe('1.00 KB'); + expect(formatSize(1048575)).toBe('1024.00 KB'); + }); + + it('handles very large numbers beyond TB', () => { + // The function only supports up to TB, so very large numbers will show as TB + expect(formatSize(1024 * 1024 * 1024 * 1024 * 1024)).toBe('1024.00 TB'); + expect(formatSize(Number.MAX_SAFE_INTEGER)).toContain('TB'); + }); + + it('handles negative numbers', () => { + // Negative numbers will result in NaN for the log calculation + expect(formatSize(-1024)).toContain('NaN'); + expect(formatSize(-0)).toBe('0 B'); + }); + + it('handles decimal numbers', () => { + expect(formatSize(1536.5)).toBe('1.50 KB'); + expect(formatSize(1023.9)).toBe('1023.9 B'); + }); + + it('handles very small positive numbers', () => { + expect(formatSize(1)).toBe('1 B'); + expect(formatSize(0.5)).toBe('0.5 B'); + expect(formatSize(0.1)).toBe('0.1 B'); + }); + }); + + describe('normalizeLineEndings', () => { + it('converts CRLF to LF', () => { + expect(normalizeLineEndings('line1\r\nline2\r\nline3')).toBe('line1\nline2\nline3'); + }); + + it('leaves LF unchanged', () => { + expect(normalizeLineEndings('line1\nline2\nline3')).toBe('line1\nline2\nline3'); + }); + + it('handles mixed line endings', () => { + expect(normalizeLineEndings('line1\r\nline2\nline3\r\n')).toBe('line1\nline2\nline3\n'); + }); + + it('handles empty string', () => { + expect(normalizeLineEndings('')).toBe(''); + }); + }); + + describe('createUnifiedDiff', () => { + it('creates diff for simple changes', () => { + const original = 'line1\nline2\nline3'; + const modified = 'line1\nmodified line2\nline3'; + const diff = createUnifiedDiff(original, modified, 'test.txt'); + + expect(diff).toContain('--- test.txt'); + expect(diff).toContain('+++ test.txt'); + expect(diff).toContain('-line2'); + expect(diff).toContain('+modified line2'); + }); + + it('handles CRLF normalization', () => { + const original = 'line1\r\nline2\r\n'; + const modified = 'line1\nmodified line2\n'; + const diff = createUnifiedDiff(original, modified); + + expect(diff).toContain('-line2'); + expect(diff).toContain('+modified line2'); + }); + + it('handles identical content', () => { + const content = 'line1\nline2\nline3'; + const diff = createUnifiedDiff(content, content); + + // Should not contain any +/- lines for identical content (excluding header lines) + expect(diff.split('\n').filter((line: string) => line.startsWith('+++') || line.startsWith('---'))).toHaveLength(2); + expect(diff.split('\n').filter((line: string) => line.startsWith('+') && !line.startsWith('+++'))).toHaveLength(0); + expect(diff.split('\n').filter((line: string) => line.startsWith('-') && !line.startsWith('---'))).toHaveLength(0); + }); + + it('handles empty content', () => { + const diff = createUnifiedDiff('', ''); + expect(diff).toContain('--- file'); + expect(diff).toContain('+++ file'); + }); + + it('handles default filename parameter', () => { + const diff = createUnifiedDiff('old', 'new'); + expect(diff).toContain('--- file'); + expect(diff).toContain('+++ file'); + }); + + it('handles custom filename', () => { + const diff = createUnifiedDiff('old', 'new', 'custom.txt'); + expect(diff).toContain('--- custom.txt'); + expect(diff).toContain('+++ custom.txt'); + }); + }); + }); + + describe('Security & Validation Functions', () => { + describe('validatePath', () => { + // Use Windows-compatible paths for testing + const allowedDirs = process.platform === 'win32' ? ['C:\\Users\\test', 'C:\\temp'] : ['/home/user', '/tmp']; + + beforeEach(() => { + mockFs.realpath.mockImplementation(async (path: any) => path.toString()); + }); + + it('validates allowed paths', async () => { + const testPath = process.platform === 'win32' ? 'C:\\Users\\test\\file.txt' : '/home/user/file.txt'; + const result = await validatePath(testPath); + expect(result).toBe(testPath); + }); + + it('rejects disallowed paths', async () => { + const testPath = process.platform === 'win32' ? 'C:\\Windows\\System32\\file.txt' : '/etc/passwd'; + await expect(validatePath(testPath)) + .rejects.toThrow('Access denied - path outside allowed directories'); + }); + + it('handles non-existent files by checking parent directory', async () => { + const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\newfile.txt' : '/home/user/newfile.txt'; + const parentPath = process.platform === 'win32' ? 'C:\\Users\\test' : '/home/user'; + + // Create an error with the ENOENT code that the implementation checks for + const enoentError = new Error('ENOENT') as NodeJS.ErrnoException; + enoentError.code = 'ENOENT'; + + mockFs.realpath + .mockRejectedValueOnce(enoentError) + .mockResolvedValueOnce(parentPath); + + const result = await validatePath(newFilePath); + expect(result).toBe(path.resolve(newFilePath)); + }); + + it('rejects when parent directory does not exist', async () => { + const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\nonexistent\\newfile.txt' : '/home/user/nonexistent/newfile.txt'; + + // Create errors with the ENOENT code + const enoentError1 = new Error('ENOENT') as NodeJS.ErrnoException; + enoentError1.code = 'ENOENT'; + const enoentError2 = new Error('ENOENT') as NodeJS.ErrnoException; + enoentError2.code = 'ENOENT'; + + mockFs.realpath + .mockRejectedValueOnce(enoentError1) + .mockRejectedValueOnce(enoentError2); + + await expect(validatePath(newFilePath)) + .rejects.toThrow('Parent directory does not exist'); + }); + + it('resolves relative paths against allowed directories instead of process.cwd()', async () => { + const relativePath = 'test-file.txt'; + const originalCwd = process.cwd; + + // Mock process.cwd to return a directory outside allowed directories + const disallowedCwd = process.platform === 'win32' ? 'C:\\Windows\\System32' : '/root'; + (process as any).cwd = vi.fn(() => disallowedCwd); + + try { + const result = await validatePath(relativePath); + + // Result should be resolved against first allowed directory, not process.cwd() + const expectedPath = process.platform === 'win32' + ? path.resolve('C:\\Users\\test', relativePath) + : path.resolve('/home/user', relativePath); + + expect(result).toBe(expectedPath); + expect(result).not.toContain(disallowedCwd); + } finally { + // Restore original process.cwd + process.cwd = originalCwd; + } + }); + }); + }); + + describe('File Operations', () => { + describe('getFileStats', () => { + it('returns file statistics', async () => { + const mockStats = { + size: 1024, + birthtime: new Date('2023-01-01'), + mtime: new Date('2023-01-02'), + atime: new Date('2023-01-03'), + isDirectory: () => false, + isFile: () => true, + mode: 0o644 + }; + + mockFs.stat.mockResolvedValueOnce(mockStats as any); + + const result = await getFileStats('/test/file.txt'); + + expect(result).toEqual({ + size: 1024, + created: new Date('2023-01-01'), + modified: new Date('2023-01-02'), + accessed: new Date('2023-01-03'), + isDirectory: false, + isFile: true, + permissions: '644' + }); + }); + + it('handles directory statistics', async () => { + const mockStats = { + size: 4096, + birthtime: new Date('2023-01-01'), + mtime: new Date('2023-01-02'), + atime: new Date('2023-01-03'), + isDirectory: () => true, + isFile: () => false, + mode: 0o755 + }; + + mockFs.stat.mockResolvedValueOnce(mockStats as any); + + const result = await getFileStats('/test/dir'); + + expect(result.isDirectory).toBe(true); + expect(result.isFile).toBe(false); + expect(result.permissions).toBe('755'); + }); + }); + + describe('readFileContent', () => { + it('reads file with default encoding', async () => { + mockFs.readFile.mockResolvedValueOnce('file content'); + + const result = await readFileContent('/test/file.txt'); + + expect(result).toBe('file content'); + expect(mockFs.readFile).toHaveBeenCalledWith('/test/file.txt', 'utf-8'); + }); + + it('reads file with custom encoding', async () => { + mockFs.readFile.mockResolvedValueOnce('file content'); + + const result = await readFileContent('/test/file.txt', 'ascii'); + + expect(result).toBe('file content'); + expect(mockFs.readFile).toHaveBeenCalledWith('/test/file.txt', 'ascii'); + }); + }); + + describe('writeFileContent', () => { + it('writes file content', async () => { + mockFs.writeFile.mockResolvedValueOnce(undefined); + + await writeFileContent('/test/file.txt', 'new content'); + + expect(mockFs.writeFile).toHaveBeenCalledWith('/test/file.txt', 'new content', { encoding: "utf-8", flag: 'wx' }); + }); + }); + + }); + + describe('Search & Filtering Functions', () => { + describe('searchFilesWithValidation', () => { + beforeEach(() => { + mockFs.realpath.mockImplementation(async (path: any) => path.toString()); + }); + + + it('excludes files matching exclude patterns', async () => { + const mockEntries = [ + { name: 'test.txt', isDirectory: () => false }, + { name: 'test.log', isDirectory: () => false }, + { name: 'node_modules', isDirectory: () => true } + ]; + + mockFs.readdir.mockResolvedValueOnce(mockEntries as any); + + const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir'; + const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed']; + + // Mock realpath to return the same path for validation to pass + mockFs.realpath.mockImplementation(async (inputPath: any) => { + const pathStr = inputPath.toString(); + // Return the path as-is for validation + return pathStr; + }); + + const result = await searchFilesWithValidation( + testDir, + '*test*', + allowedDirs, + { excludePatterns: ['*.log', 'node_modules'] } + ); + + const expectedResult = process.platform === 'win32' ? 'C:\\allowed\\dir\\test.txt' : '/allowed/dir/test.txt'; + expect(result).toEqual([expectedResult]); + }); + + it('handles validation errors during search', async () => { + const mockEntries = [ + { name: 'test.txt', isDirectory: () => false }, + { name: 'invalid_file.txt', isDirectory: () => false } + ]; + + mockFs.readdir.mockResolvedValueOnce(mockEntries as any); + + // Mock validatePath to throw error for invalid_file.txt + mockFs.realpath.mockImplementation(async (path: any) => { + if (path.toString().includes('invalid_file.txt')) { + throw new Error('Access denied'); + } + return path.toString(); + }); + + const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir'; + const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed']; + + const result = await searchFilesWithValidation( + testDir, + '*test*', + allowedDirs, + {} + ); + + // Should only return the valid file, skipping the invalid one + const expectedResult = process.platform === 'win32' ? 'C:\\allowed\\dir\\test.txt' : '/allowed/dir/test.txt'; + expect(result).toEqual([expectedResult]); + }); + + it('handles complex exclude patterns with wildcards', async () => { + const mockEntries = [ + { name: 'test.txt', isDirectory: () => false }, + { name: 'test.backup', isDirectory: () => false }, + { name: 'important_test.js', isDirectory: () => false } + ]; + + mockFs.readdir.mockResolvedValueOnce(mockEntries as any); + + const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir'; + const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed']; + + const result = await searchFilesWithValidation( + testDir, + '*test*', + allowedDirs, + { excludePatterns: ['*.backup'] } + ); + + const expectedResults = process.platform === 'win32' ? [ + 'C:\\allowed\\dir\\test.txt', + 'C:\\allowed\\dir\\important_test.js' + ] : [ + '/allowed/dir/test.txt', + '/allowed/dir/important_test.js' + ]; + expect(result).toEqual(expectedResults); + }); + }); + }); + + describe('File Editing Functions', () => { + describe('applyFileEdits', () => { + beforeEach(() => { + mockFs.readFile.mockResolvedValue('line1\nline2\nline3\n'); + mockFs.writeFile.mockResolvedValue(undefined); + }); + + it('applies simple text replacement', async () => { + const edits = [ + { oldText: 'line2', newText: 'modified line2' } + ]; + + mockFs.rename.mockResolvedValueOnce(undefined); + + const result = await applyFileEdits('/test/file.txt', edits, false); + + expect(result).toContain('modified line2'); + // Should write to temporary file then rename + expect(mockFs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + 'line1\nmodified line2\nline3\n', + 'utf-8' + ); + expect(mockFs.rename).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + '/test/file.txt' + ); + }); + + it('handles dry run mode', async () => { + const edits = [ + { oldText: 'line2', newText: 'modified line2' } + ]; + + const result = await applyFileEdits('/test/file.txt', edits, true); + + expect(result).toContain('modified line2'); + expect(mockFs.writeFile).not.toHaveBeenCalled(); + }); + + it('applies multiple edits sequentially', async () => { + const edits = [ + { oldText: 'line1', newText: 'first line' }, + { oldText: 'line3', newText: 'third line' } + ]; + + mockFs.rename.mockResolvedValueOnce(undefined); + + await applyFileEdits('/test/file.txt', edits, false); + + expect(mockFs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + 'first line\nline2\nthird line\n', + 'utf-8' + ); + expect(mockFs.rename).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + '/test/file.txt' + ); + }); + + it('handles whitespace-flexible matching', async () => { + mockFs.readFile.mockResolvedValue(' line1\n line2\n line3\n'); + + const edits = [ + { oldText: 'line2', newText: 'modified line2' } + ]; + + mockFs.rename.mockResolvedValueOnce(undefined); + + await applyFileEdits('/test/file.txt', edits, false); + + expect(mockFs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + ' line1\n modified line2\n line3\n', + 'utf-8' + ); + expect(mockFs.rename).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + '/test/file.txt' + ); + }); + + it('throws error for non-matching edits', async () => { + const edits = [ + { oldText: 'nonexistent line', newText: 'replacement' } + ]; + + await expect(applyFileEdits('/test/file.txt', edits, false)) + .rejects.toThrow('Could not find exact match for edit'); + }); + + it('handles complex multi-line edits with indentation', async () => { + mockFs.readFile.mockResolvedValue('function test() {\n console.log("hello");\n return true;\n}'); + + const edits = [ + { + oldText: ' console.log("hello");\n return true;', + newText: ' console.log("world");\n console.log("test");\n return false;' + } + ]; + + mockFs.rename.mockResolvedValueOnce(undefined); + + await applyFileEdits('/test/file.js', edits, false); + + expect(mockFs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), + 'function test() {\n console.log("world");\n console.log("test");\n return false;\n}', + 'utf-8' + ); + expect(mockFs.rename).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), + '/test/file.js' + ); + }); + + it('handles edits with different indentation patterns', async () => { + mockFs.readFile.mockResolvedValue(' if (condition) {\n doSomething();\n }'); + + const edits = [ + { + oldText: 'doSomething();', + newText: 'doSomethingElse();\n doAnotherThing();' + } + ]; + + mockFs.rename.mockResolvedValueOnce(undefined); + + await applyFileEdits('/test/file.js', edits, false); + + expect(mockFs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), + ' if (condition) {\n doSomethingElse();\n doAnotherThing();\n }', + 'utf-8' + ); + expect(mockFs.rename).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/), + '/test/file.js' + ); + }); + + it('handles CRLF line endings in file content', async () => { + mockFs.readFile.mockResolvedValue('line1\r\nline2\r\nline3\r\n'); + + const edits = [ + { oldText: 'line2', newText: 'modified line2' } + ]; + + mockFs.rename.mockResolvedValueOnce(undefined); + + await applyFileEdits('/test/file.txt', edits, false); + + expect(mockFs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + 'line1\nmodified line2\nline3\n', + 'utf-8' + ); + expect(mockFs.rename).toHaveBeenCalledWith( + expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/), + '/test/file.txt' + ); + }); + }); + + describe('tailFile', () => { + it('handles empty files', async () => { + mockFs.stat.mockResolvedValue({ size: 0 } as any); + + const result = await tailFile('/test/empty.txt', 5); + + expect(result).toBe(''); + expect(mockFs.open).not.toHaveBeenCalled(); + }); + + it('calls stat to check file size', async () => { + mockFs.stat.mockResolvedValue({ size: 100 } as any); + + // Mock file handle with proper typing + const mockFileHandle = { + read: vi.fn(), + close: vi.fn() + } as any; + + mockFileHandle.read.mockResolvedValue({ bytesRead: 0 }); + mockFileHandle.close.mockResolvedValue(undefined); + + mockFs.open.mockResolvedValue(mockFileHandle); + + await tailFile('/test/file.txt', 2); + + expect(mockFs.stat).toHaveBeenCalledWith('/test/file.txt'); + expect(mockFs.open).toHaveBeenCalledWith('/test/file.txt', 'r'); + }); + + it('handles files with content and returns last lines', async () => { + mockFs.stat.mockResolvedValue({ size: 50 } as any); + + const mockFileHandle = { + read: vi.fn(), + close: vi.fn() + } as any; + + // Simulate reading file content in chunks + mockFileHandle.read + .mockResolvedValueOnce({ bytesRead: 20, buffer: Buffer.from('line3\nline4\nline5\n') }) + .mockResolvedValueOnce({ bytesRead: 0 }); + mockFileHandle.close.mockResolvedValue(undefined); + + mockFs.open.mockResolvedValue(mockFileHandle); + + const result = await tailFile('/test/file.txt', 2); + + expect(mockFileHandle.close).toHaveBeenCalled(); + }); + + it('handles read errors gracefully', async () => { + mockFs.stat.mockResolvedValue({ size: 100 } as any); + + const mockFileHandle = { + read: vi.fn(), + close: vi.fn() + } as any; + + mockFileHandle.read.mockResolvedValue({ bytesRead: 0 }); + mockFileHandle.close.mockResolvedValue(undefined); + + mockFs.open.mockResolvedValue(mockFileHandle); + + await tailFile('/test/file.txt', 5); + + expect(mockFileHandle.close).toHaveBeenCalled(); + }); + }); + + describe('headFile', () => { + it('opens file for reading', async () => { + // Mock file handle with proper typing + const mockFileHandle = { + read: vi.fn(), + close: vi.fn() + } as any; + + mockFileHandle.read.mockResolvedValue({ bytesRead: 0 }); + mockFileHandle.close.mockResolvedValue(undefined); + + mockFs.open.mockResolvedValue(mockFileHandle); + + await headFile('/test/file.txt', 2); + + expect(mockFs.open).toHaveBeenCalledWith('/test/file.txt', 'r'); + }); + + it('handles files with content and returns first lines', async () => { + const mockFileHandle = { + read: vi.fn(), + close: vi.fn() + } as any; + + // Simulate reading file content with newlines + mockFileHandle.read + .mockResolvedValueOnce({ bytesRead: 20, buffer: Buffer.from('line1\nline2\nline3\n') }) + .mockResolvedValueOnce({ bytesRead: 0 }); + mockFileHandle.close.mockResolvedValue(undefined); + + mockFs.open.mockResolvedValue(mockFileHandle); + + const result = await headFile('/test/file.txt', 2); + + expect(mockFileHandle.close).toHaveBeenCalled(); + }); + + it('handles files with leftover content', async () => { + const mockFileHandle = { + read: vi.fn(), + close: vi.fn() + } as any; + + // Simulate reading file content without final newline + mockFileHandle.read + .mockResolvedValueOnce({ bytesRead: 15, buffer: Buffer.from('line1\nline2\nend') }) + .mockResolvedValueOnce({ bytesRead: 0 }); + mockFileHandle.close.mockResolvedValue(undefined); + + mockFs.open.mockResolvedValue(mockFileHandle); + + const result = await headFile('/test/file.txt', 5); + + expect(mockFileHandle.close).toHaveBeenCalled(); + }); + + it('handles reaching requested line count', async () => { + const mockFileHandle = { + read: vi.fn(), + close: vi.fn() + } as any; + + // Simulate reading exactly the requested number of lines + mockFileHandle.read + .mockResolvedValueOnce({ bytesRead: 12, buffer: Buffer.from('line1\nline2\n') }) + .mockResolvedValueOnce({ bytesRead: 0 }); + mockFileHandle.close.mockResolvedValue(undefined); + + mockFs.open.mockResolvedValue(mockFileHandle); + + const result = await headFile('/test/file.txt', 2); + + expect(mockFileHandle.close).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-utils.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-utils.test.ts new file mode 100644 index 00000000..5530cba1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-utils.test.ts @@ -0,0 +1,371 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { normalizePath, expandHome, convertToWindowsPath } from '../path-utils.js'; + +describe('Path Utilities', () => { + describe('convertToWindowsPath', () => { + it('leaves Unix paths unchanged', () => { + expect(convertToWindowsPath('/usr/local/bin')) + .toBe('/usr/local/bin'); + expect(convertToWindowsPath('/home/user/some path')) + .toBe('/home/user/some path'); + }); + + it('never converts WSL paths (they work correctly in WSL with Node.js fs)', () => { + // WSL paths should NEVER be converted, regardless of platform + // They are valid Linux paths that work with Node.js fs operations inside WSL + expect(convertToWindowsPath('/mnt/c/NS/MyKindleContent')) + .toBe('/mnt/c/NS/MyKindleContent'); + expect(convertToWindowsPath('/mnt/d/Documents')) + .toBe('/mnt/d/Documents'); + }); + + it('converts Unix-style Windows paths only on Windows platform', () => { + // On Windows, /c/ style paths should be converted + if (process.platform === 'win32') { + expect(convertToWindowsPath('/c/NS/MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + } else { + // On Linux, leave them unchanged + expect(convertToWindowsPath('/c/NS/MyKindleContent')) + .toBe('/c/NS/MyKindleContent'); + } + }); + + it('leaves Windows paths unchanged but ensures backslashes', () => { + expect(convertToWindowsPath('C:\\NS\\MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + expect(convertToWindowsPath('C:/NS/MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + }); + + it('handles Windows paths with spaces', () => { + expect(convertToWindowsPath('C:\\Program Files\\Some App')) + .toBe('C:\\Program Files\\Some App'); + expect(convertToWindowsPath('C:/Program Files/Some App')) + .toBe('C:\\Program Files\\Some App'); + }); + + it('handles drive letter paths based on platform', () => { + // WSL paths should never be converted + expect(convertToWindowsPath('/mnt/d/some/path')) + .toBe('/mnt/d/some/path'); + + if (process.platform === 'win32') { + // On Windows, Unix-style paths like /d/ should be converted + expect(convertToWindowsPath('/d/some/path')) + .toBe('D:\\some\\path'); + } else { + // On Linux, /d/ is just a regular Unix path + expect(convertToWindowsPath('/d/some/path')) + .toBe('/d/some/path'); + } + }); + }); + + describe('normalizePath', () => { + it('preserves Unix paths', () => { + expect(normalizePath('/usr/local/bin')) + .toBe('/usr/local/bin'); + expect(normalizePath('/home/user/some path')) + .toBe('/home/user/some path'); + expect(normalizePath('"/usr/local/some app/"')) + .toBe('/usr/local/some app'); + expect(normalizePath('/usr/local//bin/app///')) + .toBe('/usr/local/bin/app'); + expect(normalizePath('/')) + .toBe('/'); + expect(normalizePath('///')) + .toBe('/'); + }); + + it('removes surrounding quotes', () => { + expect(normalizePath('"C:\\NS\\My Kindle Content"')) + .toBe('C:\\NS\\My Kindle Content'); + }); + + it('normalizes backslashes', () => { + expect(normalizePath('C:\\\\NS\\\\MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + }); + + it('converts forward slashes to backslashes on Windows', () => { + expect(normalizePath('C:/NS/MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + }); + + it('always preserves WSL paths (they work correctly in WSL)', () => { + // WSL paths should ALWAYS be preserved, regardless of platform + // This is the fix for issue #2795 + expect(normalizePath('/mnt/c/NS/MyKindleContent')) + .toBe('/mnt/c/NS/MyKindleContent'); + expect(normalizePath('/mnt/d/Documents')) + .toBe('/mnt/d/Documents'); + }); + + it('handles Unix-style Windows paths', () => { + // On Windows, /c/ paths should be converted + if (process.platform === 'win32') { + expect(normalizePath('/c/NS/MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + } else if (process.platform === 'linux') { + // On Linux, /c/ is just a regular Unix path + expect(normalizePath('/c/NS/MyKindleContent')) + .toBe('/c/NS/MyKindleContent'); + } + }); + + it('handles paths with spaces and mixed slashes', () => { + expect(normalizePath('C:/NS/My Kindle Content')) + .toBe('C:\\NS\\My Kindle Content'); + // WSL paths should always be preserved + expect(normalizePath('/mnt/c/NS/My Kindle Content')) + .toBe('/mnt/c/NS/My Kindle Content'); + expect(normalizePath('C:\\Program Files (x86)\\App Name')) + .toBe('C:\\Program Files (x86)\\App Name'); + expect(normalizePath('"C:\\Program Files\\App Name"')) + .toBe('C:\\Program Files\\App Name'); + expect(normalizePath(' C:\\Program Files\\App Name ')) + .toBe('C:\\Program Files\\App Name'); + }); + + it('preserves spaces in all path formats', () => { + // WSL paths should always be preserved + expect(normalizePath('/mnt/c/Program Files/App Name')) + .toBe('/mnt/c/Program Files/App Name'); + + if (process.platform === 'win32') { + // On Windows, Unix-style paths like /c/ should be converted + expect(normalizePath('/c/Program Files/App Name')) + .toBe('C:\\Program Files\\App Name'); + } else { + // On Linux, /c/ is just a regular Unix path + expect(normalizePath('/c/Program Files/App Name')) + .toBe('/c/Program Files/App Name'); + } + expect(normalizePath('C:/Program Files/App Name')) + .toBe('C:\\Program Files\\App Name'); + }); + + it('handles special characters in paths', () => { + // Test ampersand in path + expect(normalizePath('C:\\NS\\Sub&Folder')) + .toBe('C:\\NS\\Sub&Folder'); + expect(normalizePath('C:/NS/Sub&Folder')) + .toBe('C:\\NS\\Sub&Folder'); + // WSL paths should always be preserved + expect(normalizePath('/mnt/c/NS/Sub&Folder')) + .toBe('/mnt/c/NS/Sub&Folder'); + + // Test tilde in path (short names in Windows) + expect(normalizePath('C:\\NS\\MYKIND~1')) + .toBe('C:\\NS\\MYKIND~1'); + expect(normalizePath('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1')) + .toBe('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1'); + + // Test other special characters + expect(normalizePath('C:\\Path with #hash')) + .toBe('C:\\Path with #hash'); + expect(normalizePath('C:\\Path with (parentheses)')) + .toBe('C:\\Path with (parentheses)'); + expect(normalizePath('C:\\Path with [brackets]')) + .toBe('C:\\Path with [brackets]'); + expect(normalizePath('C:\\Path with @at+plus$dollar%percent')) + .toBe('C:\\Path with @at+plus$dollar%percent'); + }); + + it('capitalizes lowercase drive letters for Windows paths', () => { + expect(normalizePath('c:/windows/system32')) + .toBe('C:\\windows\\system32'); + // WSL paths should always be preserved + expect(normalizePath('/mnt/d/my/folder')) + .toBe('/mnt/d/my/folder'); + + if (process.platform === 'win32') { + // On Windows, Unix-style paths should be converted and capitalized + expect(normalizePath('/e/another/folder')) + .toBe('E:\\another\\folder'); + } else { + // On Linux, /e/ is just a regular Unix path + expect(normalizePath('/e/another/folder')) + .toBe('/e/another/folder'); + } + }); + + it('handles UNC paths correctly', () => { + // UNC paths should preserve the leading double backslash + const uncPath = '\\\\SERVER\\share\\folder'; + expect(normalizePath(uncPath)).toBe('\\\\SERVER\\share\\folder'); + + // Test UNC path with double backslashes that need normalization + const uncPathWithDoubles = '\\\\\\\\SERVER\\\\share\\\\folder'; + expect(normalizePath(uncPathWithDoubles)).toBe('\\\\SERVER\\share\\folder'); + }); + + it('returns normalized non-Windows/WSL/Unix-style Windows paths as is after basic normalization', () => { + // A path that looks somewhat absolute but isn't a drive or recognized Unix root for Windows conversion + // These paths should be preserved as-is (not converted to Windows C:\ format or WSL format) + const otherAbsolutePath = '\\someserver\\share\\file'; + expect(normalizePath(otherAbsolutePath)).toBe(otherAbsolutePath); + }); + }); + + describe('expandHome', () => { + it('expands ~ to home directory', () => { + const result = expandHome('~/test'); + expect(result).toContain('test'); + expect(result).not.toContain('~'); + }); + + it('expands bare ~ to home directory', () => { + const result = expandHome('~'); + expect(result).not.toContain('~'); + expect(result.length).toBeGreaterThan(0); + }); + + it('leaves other paths unchanged', () => { + expect(expandHome('C:/test')).toBe('C:/test'); + }); + }); + + describe('WSL path handling (issue #2795 fix)', () => { + // Save original platform + const originalPlatform = process.platform; + + afterEach(() => { + // Restore platform after each test + Object.defineProperty(process, 'platform', { + value: originalPlatform, + writable: true, + configurable: true + }); + }); + + it('should NEVER convert WSL paths - they work correctly in WSL with Node.js fs', () => { + // The key insight: When running `wsl npx ...`, Node.js runs INSIDE WSL (process.platform === 'linux') + // and /mnt/c/ paths work correctly with Node.js fs operations in that environment. + // Converting them to C:\ format breaks fs operations because Windows paths don't work inside WSL. + + // Mock Linux platform (inside WSL) + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + // WSL paths should NOT be converted, even inside WSL + expect(normalizePath('/mnt/c/Users/username/folder')) + .toBe('/mnt/c/Users/username/folder'); + + expect(normalizePath('/mnt/d/Documents/project')) + .toBe('/mnt/d/Documents/project'); + }); + + it('should also preserve WSL paths when running on Windows', () => { + // Mock Windows platform + Object.defineProperty(process, 'platform', { + value: 'win32', + writable: true, + configurable: true + }); + + // WSL paths should still be preserved (though they wouldn't be accessible from Windows Node.js) + expect(normalizePath('/mnt/c/Users/username/folder')) + .toBe('/mnt/c/Users/username/folder'); + + expect(normalizePath('/mnt/d/Documents/project')) + .toBe('/mnt/d/Documents/project'); + }); + + it('should convert Unix-style Windows paths (/c/) only when running on Windows (win32)', () => { + // Mock process.platform to be 'win32' (Windows) + Object.defineProperty(process, 'platform', { + value: 'win32', + writable: true, + configurable: true + }); + + // Unix-style Windows paths like /c/ should be converted on Windows + expect(normalizePath('/c/Users/username/folder')) + .toBe('C:\\Users\\username\\folder'); + + expect(normalizePath('/d/Documents/project')) + .toBe('D:\\Documents\\project'); + }); + + it('should NOT convert Unix-style paths (/c/) when running inside WSL (linux)', () => { + // Mock process.platform to be 'linux' (WSL/Linux) + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + // When on Linux, /c/ is just a regular Unix directory, not a drive letter + expect(normalizePath('/c/some/path')) + .toBe('/c/some/path'); + + expect(normalizePath('/d/another/path')) + .toBe('/d/another/path'); + }); + + it('should preserve regular Unix paths on all platforms', () => { + // Test on Linux + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + expect(normalizePath('/home/user/documents')) + .toBe('/home/user/documents'); + + expect(normalizePath('/var/log/app')) + .toBe('/var/log/app'); + + // Test on Windows (though these paths wouldn't work on Windows) + Object.defineProperty(process, 'platform', { + value: 'win32', + writable: true, + configurable: true + }); + + expect(normalizePath('/home/user/documents')) + .toBe('/home/user/documents'); + + expect(normalizePath('/var/log/app')) + .toBe('/var/log/app'); + }); + + it('reproduces exact scenario from issue #2795', () => { + // Simulate running inside WSL: wsl npx @modelcontextprotocol/server-filesystem /mnt/c/Users/username/folder + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + // This is the exact path from the issue + const inputPath = '/mnt/c/Users/username/folder'; + const result = normalizePath(inputPath); + + // Should NOT convert to C:\Users\username\folder + expect(result).toBe('/mnt/c/Users/username/folder'); + expect(result).not.toContain('C:'); + expect(result).not.toContain('\\'); + }); + + it('should handle relative path slash conversion based on platform', () => { + // This test verifies platform-specific behavior naturally without mocking + // On Windows: forward slashes converted to backslashes + // On Linux/Unix: forward slashes preserved + const relativePath = 'some/relative/path'; + const result = normalizePath(relativePath); + + if (originalPlatform === 'win32') { + expect(result).toBe('some\\relative\\path'); + } else { + expect(result).toBe('some/relative/path'); + } + }); + }); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-validation.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-validation.test.ts new file mode 100644 index 00000000..81ad247e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-validation.test.ts @@ -0,0 +1,1000 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as path from 'path'; +import * as fs from 'fs/promises'; +import * as os from 'os'; +import { isPathWithinAllowedDirectories } from '../path-validation.js'; + +/** + * Check if the current environment supports symlink creation + */ +async function checkSymlinkSupport(): Promise { + const testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'symlink-test-')); + try { + const targetFile = path.join(testDir, 'target.txt'); + const linkFile = path.join(testDir, 'link.txt'); + + await fs.writeFile(targetFile, 'test'); + await fs.symlink(targetFile, linkFile); + + // If we get here, symlinks are supported + return true; + } catch (error) { + // EPERM indicates no symlink permissions + if ((error as NodeJS.ErrnoException).code === 'EPERM') { + return false; + } + // Other errors might indicate a real problem + throw error; + } finally { + await fs.rm(testDir, { recursive: true, force: true }); + } +} + +// Global variable to store symlink support status +let symlinkSupported: boolean | null = null; + +/** + * Get cached symlink support status, checking once per test run + */ +async function getSymlinkSupport(): Promise { + if (symlinkSupported === null) { + symlinkSupported = await checkSymlinkSupport(); + if (!symlinkSupported) { + console.log('\n⚠️ Symlink tests will be skipped - symlink creation not supported in this environment'); + console.log(' On Windows, enable Developer Mode or run as Administrator to enable symlink tests'); + } + } + return symlinkSupported; +} + +describe('Path Validation', () => { + it('allows exact directory match', () => { + const allowed = ['/home/user/project']; + expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true); + }); + + it('allows subdirectories', () => { + const allowed = ['/home/user/project']; + expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/src/index.js', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/deeply/nested/file.txt', allowed)).toBe(true); + }); + + it('blocks similar directory names (prefix vulnerability)', () => { + const allowed = ['/home/user/project']; + expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project_backup', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project-old', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/projectile', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project.bak', allowed)).toBe(false); + }); + + it('blocks paths outside allowed directories', () => { + const allowed = ['/home/user/project']; + expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/etc/passwd', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/', allowed)).toBe(false); + }); + + it('handles multiple allowed directories', () => { + const allowed = ['/home/user/project1', '/home/user/project2']; + expect(isPathWithinAllowedDirectories('/home/user/project1/src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project2/src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project3', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project1_backup', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project2-old', allowed)).toBe(false); + }); + + it('blocks parent and sibling directories', () => { + const allowed = ['/test/allowed']; + + // Parent directory + expect(isPathWithinAllowedDirectories('/test', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/', allowed)).toBe(false); + + // Sibling with common prefix + expect(isPathWithinAllowedDirectories('/test/allowed_sibling', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/test/allowed2', allowed)).toBe(false); + }); + + it('handles paths with special characters', () => { + const allowed = ['/home/user/my-project (v2)']; + + expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)/src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)_backup', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/my-project', allowed)).toBe(false); + }); + + describe('Input validation', () => { + it('rejects empty inputs', () => { + const allowed = ['/home/user/project']; + + expect(isPathWithinAllowedDirectories('', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project', [])).toBe(false); + }); + + it('handles trailing separators correctly', () => { + const allowed = ['/home/user/project']; + + // Path with trailing separator should still match + expect(isPathWithinAllowedDirectories('/home/user/project/', allowed)).toBe(true); + + // Allowed directory with trailing separator + const allowedWithSep = ['/home/user/project/']; + expect(isPathWithinAllowedDirectories('/home/user/project', allowedWithSep)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/', allowedWithSep)).toBe(true); + + // Should still block similar names with or without trailing separators + expect(isPathWithinAllowedDirectories('/home/user/project2', allowedWithSep)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project2/', allowed)).toBe(false); + }); + + it('skips empty directory entries in allowed list', () => { + const allowed = ['', '/home/user/project', '']; + expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true); + + // Should still validate properly with empty entries + expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false); + }); + + it('handles Windows paths with trailing separators', () => { + if (path.sep === '\\') { + const allowed = ['C:\\Users\\project']; + + // Path with trailing separator + expect(isPathWithinAllowedDirectories('C:\\Users\\project\\', allowed)).toBe(true); + + // Allowed with trailing separator + const allowedWithSep = ['C:\\Users\\project\\']; + expect(isPathWithinAllowedDirectories('C:\\Users\\project', allowedWithSep)).toBe(true); + expect(isPathWithinAllowedDirectories('C:\\Users\\project\\', allowedWithSep)).toBe(true); + + // Should still block similar names + expect(isPathWithinAllowedDirectories('C:\\Users\\project2\\', allowed)).toBe(false); + } + }); + }); + + describe('Error handling', () => { + it('normalizes relative paths to absolute', () => { + const allowed = [process.cwd()]; + + // Relative paths get normalized to absolute paths based on cwd + expect(isPathWithinAllowedDirectories('relative/path', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('./file', allowed)).toBe(true); + + // Parent directory references that escape allowed directory + const parentAllowed = ['/home/user/project']; + expect(isPathWithinAllowedDirectories('../parent', parentAllowed)).toBe(false); + }); + + it('returns false for relative paths in allowed directories', () => { + const badAllowed = ['relative/path', '/some/other/absolute/path']; + + // Relative paths in allowed dirs are normalized to absolute based on cwd + // The normalized 'relative/path' won't match our test path + expect(isPathWithinAllowedDirectories('/some/other/absolute/path/file', badAllowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/absolute/path/file', badAllowed)).toBe(false); + }); + + it('handles null and undefined inputs gracefully', () => { + const allowed = ['/home/user/project']; + + // Should return false, not crash + expect(isPathWithinAllowedDirectories(null as any, allowed)).toBe(false); + expect(isPathWithinAllowedDirectories(undefined as any, allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/path', null as any)).toBe(false); + expect(isPathWithinAllowedDirectories('/path', undefined as any)).toBe(false); + }); + }); + + describe('Unicode and special characters', () => { + it('handles unicode characters in paths', () => { + const allowed = ['/home/user/café']; + + expect(isPathWithinAllowedDirectories('/home/user/café', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/café/file', allowed)).toBe(true); + + // Different unicode representation won't match (not normalized) + const decomposed = '/home/user/cafe\u0301'; // e + combining accent + expect(isPathWithinAllowedDirectories(decomposed, allowed)).toBe(false); + }); + + it('handles paths with spaces correctly', () => { + const allowed = ['/home/user/my project']; + + expect(isPathWithinAllowedDirectories('/home/user/my project', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/my project/file', allowed)).toBe(true); + + // Partial matches should fail + expect(isPathWithinAllowedDirectories('/home/user/my', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/my proj', allowed)).toBe(false); + }); + }); + + describe('Overlapping allowed directories', () => { + it('handles nested allowed directories correctly', () => { + const allowed = ['/home', '/home/user', '/home/user/project']; + + // All paths under /home are allowed + expect(isPathWithinAllowedDirectories('/home/anything', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/anything', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/anything', allowed)).toBe(true); + + // First match wins (most permissive) + expect(isPathWithinAllowedDirectories('/home/other/deep/path', allowed)).toBe(true); + }); + + it('handles root directory as allowed', () => { + const allowed = ['/']; + + // Everything is allowed under root (dangerous configuration) + expect(isPathWithinAllowedDirectories('/', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/any/path', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/etc/passwd', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/secret', allowed)).toBe(true); + + // But only on the same filesystem root + if (path.sep === '\\') { + expect(isPathWithinAllowedDirectories('D:\\other', ['/'])).toBe(false); + } + }); + }); + + describe('Cross-platform behavior', () => { + it('handles Windows-style paths on Windows', () => { + if (path.sep === '\\') { + const allowed = ['C:\\Users\\project']; + expect(isPathWithinAllowedDirectories('C:\\Users\\project', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('C:\\Users\\project\\src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('C:\\Users\\project2', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('C:\\Users\\project_backup', allowed)).toBe(false); + } + }); + + it('handles Unix-style paths on Unix', () => { + if (path.sep === '/') { + const allowed = ['/home/user/project']; + expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false); + } + }); + }); + + describe('Validation Tests - Path Traversal', () => { + it('blocks path traversal attempts', () => { + const allowed = ['/home/user/project']; + + // Basic traversal attempts + expect(isPathWithinAllowedDirectories('/home/user/project/../../../etc/passwd', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project/../../other', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project/../project2', allowed)).toBe(false); + + // Mixed traversal with valid segments + expect(isPathWithinAllowedDirectories('/home/user/project/src/../../project2', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project/./../../other', allowed)).toBe(false); + + // Multiple traversal sequences + expect(isPathWithinAllowedDirectories('/home/user/project/../project/../../../etc', allowed)).toBe(false); + }); + + it('blocks traversal in allowed directories', () => { + const allowed = ['/home/user/project/../safe']; + + // The allowed directory itself should be normalized and safe + expect(isPathWithinAllowedDirectories('/home/user/safe/file', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(false); + }); + + it('handles complex traversal patterns', () => { + const allowed = ['/home/user/project']; + + // Double dots in filenames (not traversal) - these normalize to paths within allowed dir + expect(isPathWithinAllowedDirectories('/home/user/project/..test', allowed)).toBe(true); // Not traversal + expect(isPathWithinAllowedDirectories('/home/user/project/test..', allowed)).toBe(true); // Not traversal + expect(isPathWithinAllowedDirectories('/home/user/project/te..st', allowed)).toBe(true); // Not traversal + + // Actual traversal + expect(isPathWithinAllowedDirectories('/home/user/project/../test', allowed)).toBe(false); // Is traversal - goes to /home/user/test + + // Edge case: /home/user/project/.. normalizes to /home/user (parent dir) + expect(isPathWithinAllowedDirectories('/home/user/project/..', allowed)).toBe(false); // Goes to parent + }); + }); + + describe('Validation Tests - Null Bytes', () => { + it('rejects paths with null bytes', () => { + const allowed = ['/home/user/project']; + + expect(isPathWithinAllowedDirectories('/home/user/project\x00/etc/passwd', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project/test\x00.txt', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('\x00/home/user/project', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project/\x00', allowed)).toBe(false); + }); + + it('rejects allowed directories with null bytes', () => { + const allowed = ['/home/user/project\x00']; + + expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(false); + }); + }); + + describe('Validation Tests - Special Characters', () => { + it('allows percent signs in filenames', () => { + const allowed = ['/home/user/project']; + + // Percent is a valid filename character + expect(isPathWithinAllowedDirectories('/home/user/project/report_50%.pdf', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/Q1_25%_growth', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/%41', allowed)).toBe(true); // File named %41 + + // URL encoding is NOT decoded by path.normalize, so these are just odd filenames + expect(isPathWithinAllowedDirectories('/home/user/project/%2e%2e', allowed)).toBe(true); // File named "%2e%2e" + expect(isPathWithinAllowedDirectories('/home/user/project/file%20name', allowed)).toBe(true); // File with %20 in name + }); + + it('handles percent signs in allowed directories', () => { + const allowed = ['/home/user/project%20files']; + + // This is a directory literally named "project%20files" + expect(isPathWithinAllowedDirectories('/home/user/project%20files/test', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project files/test', allowed)).toBe(false); // Different dir + }); + }); + + describe('Path Normalization', () => { + it('normalizes paths before comparison', () => { + const allowed = ['/home/user/project']; + + // Trailing slashes + expect(isPathWithinAllowedDirectories('/home/user/project/', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project//', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project///', allowed)).toBe(true); + + // Current directory references + expect(isPathWithinAllowedDirectories('/home/user/project/./src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/./project/src', allowed)).toBe(true); + + // Multiple slashes + expect(isPathWithinAllowedDirectories('/home/user/project//src//file', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home//user//project//src', allowed)).toBe(true); + + // Should still block outside paths + expect(isPathWithinAllowedDirectories('/home/user//project2', allowed)).toBe(false); + }); + + it('handles mixed separators correctly', () => { + if (path.sep === '\\') { + const allowed = ['C:\\Users\\project']; + + // Mixed separators should be normalized + expect(isPathWithinAllowedDirectories('C:/Users/project', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('C:\\Users/project\\src', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('C:/Users\\project/src', allowed)).toBe(true); + } + }); + }); + + describe('Edge Cases', () => { + it('rejects non-string inputs safely', () => { + const allowed = ['/home/user/project']; + + expect(isPathWithinAllowedDirectories(123 as any, allowed)).toBe(false); + expect(isPathWithinAllowedDirectories({} as any, allowed)).toBe(false); + expect(isPathWithinAllowedDirectories([] as any, allowed)).toBe(false); + expect(isPathWithinAllowedDirectories(null as any, allowed)).toBe(false); + expect(isPathWithinAllowedDirectories(undefined as any, allowed)).toBe(false); + + // Non-string in allowed directories + expect(isPathWithinAllowedDirectories('/home/user/project', [123 as any])).toBe(false); + expect(isPathWithinAllowedDirectories('/home/user/project', [{} as any])).toBe(false); + }); + + it('handles very long paths', () => { + const allowed = ['/home/user/project']; + + // Create a very long path that's still valid + const longSubPath = 'a/'.repeat(1000) + 'file.txt'; + expect(isPathWithinAllowedDirectories(`/home/user/project/${longSubPath}`, allowed)).toBe(true); + + // Very long path that escapes + const escapePath = 'a/'.repeat(1000) + '../'.repeat(1001) + 'etc/passwd'; + expect(isPathWithinAllowedDirectories(`/home/user/project/${escapePath}`, allowed)).toBe(false); + }); + }); + + describe('Additional Coverage', () => { + it('handles allowed directories with traversal that normalizes safely', () => { + // These allowed dirs contain traversal but normalize to valid paths + const allowed = ['/home/user/../user/project']; + + // Should normalize to /home/user/project and work correctly + expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false); + }); + + it('handles symbolic dots in filenames', () => { + const allowed = ['/home/user/project']; + + // Single and double dots as actual filenames (not traversal) + expect(isPathWithinAllowedDirectories('/home/user/project/.', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('/home/user/project/..', allowed)).toBe(false); // This normalizes to parent + expect(isPathWithinAllowedDirectories('/home/user/project/...', allowed)).toBe(true); // Three dots is a valid filename + expect(isPathWithinAllowedDirectories('/home/user/project/....', allowed)).toBe(true); // Four dots is a valid filename + }); + + it('handles UNC paths on Windows', () => { + if (path.sep === '\\') { + const allowed = ['\\\\server\\share\\project']; + + expect(isPathWithinAllowedDirectories('\\\\server\\share\\project', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('\\\\server\\share\\project\\file', allowed)).toBe(true); + expect(isPathWithinAllowedDirectories('\\\\server\\share\\other', allowed)).toBe(false); + expect(isPathWithinAllowedDirectories('\\\\other\\share\\project', allowed)).toBe(false); + } + }); + }); + + describe('Symlink Tests', () => { + let testDir: string; + let allowedDir: string; + let forbiddenDir: string; + + beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-error-test-')); + allowedDir = path.join(testDir, 'allowed'); + forbiddenDir = path.join(testDir, 'forbidden'); + + await fs.mkdir(allowedDir, { recursive: true }); + await fs.mkdir(forbiddenDir, { recursive: true }); + }); + + afterEach(async () => { + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('validates symlink handling', async () => { + // Test with symlinks + try { + const linkPath = path.join(allowedDir, 'bad-link'); + const targetPath = path.join(forbiddenDir, 'target.txt'); + + await fs.writeFile(targetPath, 'content'); + await fs.symlink(targetPath, linkPath); + + // In real implementation, this would throw with the resolved path + const realPath = await fs.realpath(linkPath); + const allowed = [allowedDir]; + + // Symlink target should be outside allowed directory + expect(isPathWithinAllowedDirectories(realPath, allowed)).toBe(false); + } catch (error) { + // Skip if no symlink permissions + } + }); + + it('handles non-existent paths correctly', async () => { + const newFilePath = path.join(allowedDir, 'subdir', 'newfile.txt'); + + // Parent directory doesn't exist + try { + await fs.access(newFilePath); + } catch (error) { + expect((error as NodeJS.ErrnoException).code).toBe('ENOENT'); + } + + // After creating parent, validation should work + await fs.mkdir(path.dirname(newFilePath), { recursive: true }); + const allowed = [allowedDir]; + expect(isPathWithinAllowedDirectories(newFilePath, allowed)).toBe(true); + }); + + // Test path resolution consistency for symlinked files + it('validates symlinked files consistently between path and resolved forms', async () => { + try { + // Setup: Create target file in forbidden area + const targetFile = path.join(forbiddenDir, 'target.txt'); + await fs.writeFile(targetFile, 'TARGET_CONTENT'); + + // Create symlink inside allowed directory pointing to forbidden file + const symlinkPath = path.join(allowedDir, 'link-to-target.txt'); + await fs.symlink(targetFile, symlinkPath); + + // The symlink path itself passes validation (looks like it's in allowed dir) + expect(isPathWithinAllowedDirectories(symlinkPath, [allowedDir])).toBe(true); + + // But the resolved path should fail validation + const resolvedPath = await fs.realpath(symlinkPath); + expect(isPathWithinAllowedDirectories(resolvedPath, [allowedDir])).toBe(false); + + // Verify the resolved path goes to the forbidden location (normalize both paths for macOS temp dirs) + expect(await fs.realpath(resolvedPath)).toBe(await fs.realpath(targetFile)); + } catch (error) { + // Skip if no symlink permissions on the system + if ((error as NodeJS.ErrnoException).code !== 'EPERM') { + throw error; + } + } + }); + + // Test allowed directory resolution behavior + it('validates paths correctly when allowed directory is resolved from symlink', async () => { + try { + // Setup: Create the actual target directory with content + const actualTargetDir = path.join(testDir, 'actual-target'); + await fs.mkdir(actualTargetDir, { recursive: true }); + const targetFile = path.join(actualTargetDir, 'file.txt'); + await fs.writeFile(targetFile, 'FILE_CONTENT'); + + // Setup: Create symlink directory that points to target + const symlinkDir = path.join(testDir, 'symlink-dir'); + await fs.symlink(actualTargetDir, symlinkDir); + + // Simulate resolved allowed directory (what the server startup should do) + const resolvedAllowedDir = await fs.realpath(symlinkDir); + const resolvedTargetDir = await fs.realpath(actualTargetDir); + expect(resolvedAllowedDir).toBe(resolvedTargetDir); + + // Test 1: File access through original symlink path should pass validation with resolved allowed dir + const fileViaSymlink = path.join(symlinkDir, 'file.txt'); + const resolvedFile = await fs.realpath(fileViaSymlink); + expect(isPathWithinAllowedDirectories(resolvedFile, [resolvedAllowedDir])).toBe(true); + + // Test 2: File access through resolved path should also pass validation + const fileViaResolved = path.join(resolvedTargetDir, 'file.txt'); + expect(isPathWithinAllowedDirectories(fileViaResolved, [resolvedAllowedDir])).toBe(true); + + // Test 3: Demonstrate inconsistent behavior with unresolved allowed directories + // If allowed dirs were not resolved (storing symlink paths instead): + const unresolvedAllowedDirs = [symlinkDir]; + // This validation would incorrectly fail for the same content: + expect(isPathWithinAllowedDirectories(resolvedFile, unresolvedAllowedDirs)).toBe(false); + + } catch (error) { + // Skip if no symlink permissions on the system + if ((error as NodeJS.ErrnoException).code !== 'EPERM') { + throw error; + } + } + }); + + // Test for macOS /tmp -> /private/tmp symlink issue (GitHub issue #3253) + // When allowed directories include BOTH original and resolved paths, + // paths through either form should be accepted + it('allows paths through both original and resolved symlink directories', async () => { + try { + // Setup: Create the actual target directory with content + const actualTargetDir = path.join(testDir, 'actual-target'); + await fs.mkdir(actualTargetDir, { recursive: true }); + const targetFile = path.join(actualTargetDir, 'file.txt'); + await fs.writeFile(targetFile, 'FILE_CONTENT'); + + // Setup: Create symlink directory that points to target (simulates /tmp -> /private/tmp) + const symlinkDir = path.join(testDir, 'symlink-dir'); + await fs.symlink(actualTargetDir, symlinkDir); + + // Get the resolved path + const resolvedDir = await fs.realpath(symlinkDir); + + // THE FIX: Store BOTH original symlink path AND resolved path in allowed directories + // This is what the server should do during startup to fix issue #3253 + const allowedDirsWithBoth = [symlinkDir, resolvedDir]; + + // Test 1: Path through original symlink should pass validation + // (e.g., user requests /tmp/file.txt when /tmp is in allowed dirs) + const fileViaSymlink = path.join(symlinkDir, 'file.txt'); + expect(isPathWithinAllowedDirectories(fileViaSymlink, allowedDirsWithBoth)).toBe(true); + + // Test 2: Path through resolved directory should also pass validation + // (e.g., user requests /private/tmp/file.txt) + const fileViaResolved = path.join(resolvedDir, 'file.txt'); + expect(isPathWithinAllowedDirectories(fileViaResolved, allowedDirsWithBoth)).toBe(true); + + // Test 3: The resolved path of the symlink file should also pass + const resolvedFile = await fs.realpath(fileViaSymlink); + expect(isPathWithinAllowedDirectories(resolvedFile, allowedDirsWithBoth)).toBe(true); + + // Verify both paths point to the same actual file + expect(resolvedFile).toBe(await fs.realpath(fileViaResolved)); + + } catch (error) { + // Skip if no symlink permissions on the system + if ((error as NodeJS.ErrnoException).code !== 'EPERM') { + throw error; + } + } + }); + + it('resolves nested symlink chains completely', async () => { + try { + // Setup: Create target file in forbidden area + const actualTarget = path.join(forbiddenDir, 'target-file.txt'); + await fs.writeFile(actualTarget, 'FINAL_CONTENT'); + + // Create chain of symlinks: allowedFile -> link2 -> link1 -> actualTarget + const link1 = path.join(testDir, 'intermediate-link1'); + const link2 = path.join(testDir, 'intermediate-link2'); + const allowedFile = path.join(allowedDir, 'seemingly-safe-file'); + + await fs.symlink(actualTarget, link1); + await fs.symlink(link1, link2); + await fs.symlink(link2, allowedFile); + + // The allowed file path passes basic validation + expect(isPathWithinAllowedDirectories(allowedFile, [allowedDir])).toBe(true); + + // But complete resolution reveals the forbidden target + const fullyResolvedPath = await fs.realpath(allowedFile); + expect(isPathWithinAllowedDirectories(fullyResolvedPath, [allowedDir])).toBe(false); + expect(await fs.realpath(fullyResolvedPath)).toBe(await fs.realpath(actualTarget)); + + } catch (error) { + // Skip if no symlink permissions on the system + if ((error as NodeJS.ErrnoException).code !== 'EPERM') { + throw error; + } + } + }); + }); + + describe('Path Validation Race Condition Tests', () => { + let testDir: string; + let allowedDir: string; + let forbiddenDir: string; + let targetFile: string; + let testPath: string; + + beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'race-test-')); + allowedDir = path.join(testDir, 'allowed'); + forbiddenDir = path.join(testDir, 'outside'); + targetFile = path.join(forbiddenDir, 'target.txt'); + testPath = path.join(allowedDir, 'test.txt'); + + await fs.mkdir(allowedDir, { recursive: true }); + await fs.mkdir(forbiddenDir, { recursive: true }); + await fs.writeFile(targetFile, 'ORIGINAL CONTENT', 'utf-8'); + }); + + afterEach(async () => { + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('validates non-existent file paths based on parent directory', async () => { + const allowed = [allowedDir]; + + expect(isPathWithinAllowedDirectories(testPath, allowed)).toBe(true); + await expect(fs.access(testPath)).rejects.toThrow(); + + const parentDir = path.dirname(testPath); + expect(isPathWithinAllowedDirectories(parentDir, allowed)).toBe(true); + }); + + it('demonstrates symlink race condition allows writing outside allowed directories', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping symlink race condition test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + + await expect(fs.access(testPath)).rejects.toThrow(); + expect(isPathWithinAllowedDirectories(testPath, allowed)).toBe(true); + + await fs.symlink(targetFile, testPath); + await fs.writeFile(testPath, 'MODIFIED CONTENT', 'utf-8'); + + const targetContent = await fs.readFile(targetFile, 'utf-8'); + expect(targetContent).toBe('MODIFIED CONTENT'); + + const resolvedPath = await fs.realpath(testPath); + expect(isPathWithinAllowedDirectories(resolvedPath, allowed)).toBe(false); + }); + + it('shows timing differences between validation approaches', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping timing validation test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + + const validation1 = isPathWithinAllowedDirectories(testPath, allowed); + expect(validation1).toBe(true); + + await fs.symlink(targetFile, testPath); + + const resolvedPath = await fs.realpath(testPath); + const validation2 = isPathWithinAllowedDirectories(resolvedPath, allowed); + expect(validation2).toBe(false); + + expect(validation1).not.toBe(validation2); + }); + + it('validates directory creation timing', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping directory creation timing test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + const testDir = path.join(allowedDir, 'newdir'); + + expect(isPathWithinAllowedDirectories(testDir, allowed)).toBe(true); + + await fs.symlink(forbiddenDir, testDir); + + expect(isPathWithinAllowedDirectories(testDir, allowed)).toBe(true); + + const resolved = await fs.realpath(testDir); + expect(isPathWithinAllowedDirectories(resolved, allowed)).toBe(false); + }); + + it('demonstrates exclusive file creation behavior', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping exclusive file creation test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + + await fs.symlink(targetFile, testPath); + + await expect(fs.open(testPath, 'wx')).rejects.toThrow(/EEXIST/); + + await fs.writeFile(testPath, 'NEW CONTENT', 'utf-8'); + const targetContent = await fs.readFile(targetFile, 'utf-8'); + expect(targetContent).toBe('NEW CONTENT'); + }); + + it('should use resolved parent paths for non-existent files', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping resolved parent paths test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + + const symlinkDir = path.join(allowedDir, 'link'); + await fs.symlink(forbiddenDir, symlinkDir); + + const fileThroughSymlink = path.join(symlinkDir, 'newfile.txt'); + + expect(fileThroughSymlink.startsWith(allowedDir)).toBe(true); + + const parentDir = path.dirname(fileThroughSymlink); + const resolvedParent = await fs.realpath(parentDir); + expect(isPathWithinAllowedDirectories(resolvedParent, allowed)).toBe(false); + + const expectedSafePath = path.join(resolvedParent, path.basename(fileThroughSymlink)); + expect(isPathWithinAllowedDirectories(expectedSafePath, allowed)).toBe(false); + }); + + it('demonstrates parent directory symlink traversal', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping parent directory symlink traversal test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + const deepPath = path.join(allowedDir, 'sub1', 'sub2', 'file.txt'); + + expect(isPathWithinAllowedDirectories(deepPath, allowed)).toBe(true); + + const sub1Path = path.join(allowedDir, 'sub1'); + await fs.symlink(forbiddenDir, sub1Path); + + await fs.mkdir(path.join(sub1Path, 'sub2'), { recursive: true }); + await fs.writeFile(deepPath, 'CONTENT', 'utf-8'); + + const realPath = await fs.realpath(deepPath); + const realAllowedDir = await fs.realpath(allowedDir); + const realForbiddenDir = await fs.realpath(forbiddenDir); + + expect(realPath.startsWith(realAllowedDir)).toBe(false); + expect(realPath.startsWith(realForbiddenDir)).toBe(true); + }); + + it('should prevent race condition between validatePath and file operation', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping race condition prevention test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + const racePath = path.join(allowedDir, 'race-file.txt'); + const targetFile = path.join(forbiddenDir, 'target.txt'); + + await fs.writeFile(targetFile, 'ORIGINAL CONTENT', 'utf-8'); + + // Path validation would pass (file doesn't exist, parent is in allowed dir) + expect(await fs.access(racePath).then(() => false).catch(() => true)).toBe(true); + expect(isPathWithinAllowedDirectories(racePath, allowed)).toBe(true); + + // Race condition: symlink created after validation but before write + await fs.symlink(targetFile, racePath); + + // With exclusive write flag, write should fail on symlink + await expect( + fs.writeFile(racePath, 'NEW CONTENT', { encoding: 'utf-8', flag: 'wx' }) + ).rejects.toThrow(/EEXIST/); + + // Verify content unchanged + const targetContent = await fs.readFile(targetFile, 'utf-8'); + expect(targetContent).toBe('ORIGINAL CONTENT'); + + // The symlink exists but write was blocked + const actualWritePath = await fs.realpath(racePath); + expect(actualWritePath).toBe(await fs.realpath(targetFile)); + expect(isPathWithinAllowedDirectories(actualWritePath, allowed)).toBe(false); + }); + + it('should allow overwrites to legitimate files within allowed directories', async () => { + const allowed = [allowedDir]; + const legitFile = path.join(allowedDir, 'legit-file.txt'); + + // Create a legitimate file + await fs.writeFile(legitFile, 'ORIGINAL', 'utf-8'); + + // Opening with w should work for legitimate files + const fd = await fs.open(legitFile, 'w'); + try { + await fd.write('UPDATED', 0, 'utf-8'); + } finally { + await fd.close(); + } + + const content = await fs.readFile(legitFile, 'utf-8'); + expect(content).toBe('UPDATED'); + }); + + it('should handle symlinks that point within allowed directories', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping symlinks within allowed directories test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + const targetFile = path.join(allowedDir, 'target.txt'); + const symlinkPath = path.join(allowedDir, 'symlink.txt'); + + // Create target file within allowed directory + await fs.writeFile(targetFile, 'TARGET CONTENT', 'utf-8'); + + // Create symlink pointing to allowed file + await fs.symlink(targetFile, symlinkPath); + + // Opening symlink with w follows it to the target + const fd = await fs.open(symlinkPath, 'w'); + try { + await fd.write('UPDATED VIA SYMLINK', 0, 'utf-8'); + } finally { + await fd.close(); + } + + // Both symlink and target should show updated content + const symlinkContent = await fs.readFile(symlinkPath, 'utf-8'); + const targetContent = await fs.readFile(targetFile, 'utf-8'); + expect(symlinkContent).toBe('UPDATED VIA SYMLINK'); + expect(targetContent).toBe('UPDATED VIA SYMLINK'); + }); + + it('should prevent overwriting files through symlinks pointing outside allowed directories', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping symlink overwrite prevention test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + const legitFile = path.join(allowedDir, 'existing.txt'); + const targetFile = path.join(forbiddenDir, 'target.txt'); + + // Create a legitimate file first + await fs.writeFile(legitFile, 'LEGIT CONTENT', 'utf-8'); + + // Create target file in forbidden directory + await fs.writeFile(targetFile, 'FORBIDDEN CONTENT', 'utf-8'); + + // Now replace the legitimate file with a symlink to forbidden location + await fs.unlink(legitFile); + await fs.symlink(targetFile, legitFile); + + // Simulate the server's validation logic + const stats = await fs.lstat(legitFile); + expect(stats.isSymbolicLink()).toBe(true); + + const realPath = await fs.realpath(legitFile); + expect(isPathWithinAllowedDirectories(realPath, allowed)).toBe(false); + + // With atomic rename, symlinks are replaced not followed + // So this test now demonstrates the protection + + // Verify content remains unchanged + const targetContent = await fs.readFile(targetFile, 'utf-8'); + expect(targetContent).toBe('FORBIDDEN CONTENT'); + }); + + it('demonstrates race condition in read operations', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping race condition in read operations test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + const legitFile = path.join(allowedDir, 'readable.txt'); + const secretFile = path.join(forbiddenDir, 'secret.txt'); + + // Create legitimate file + await fs.writeFile(legitFile, 'PUBLIC CONTENT', 'utf-8'); + + // Create secret file in forbidden directory + await fs.writeFile(secretFile, 'SECRET CONTENT', 'utf-8'); + + // Step 1: validatePath would pass for legitimate file + expect(isPathWithinAllowedDirectories(legitFile, allowed)).toBe(true); + + // Step 2: Race condition - replace file with symlink after validation + await fs.unlink(legitFile); + await fs.symlink(secretFile, legitFile); + + // Step 3: Read operation follows symlink to forbidden location + const content = await fs.readFile(legitFile, 'utf-8'); + + // This shows the vulnerability - we read forbidden content + expect(content).toBe('SECRET CONTENT'); + expect(isPathWithinAllowedDirectories(await fs.realpath(legitFile), allowed)).toBe(false); + }); + + it('verifies rename does not follow symlinks', async () => { + const symlinkSupported = await getSymlinkSupport(); + if (!symlinkSupported) { + console.log(' ⏭️ Skipping rename symlink test - symlinks not supported'); + return; + } + + const allowed = [allowedDir]; + const tempFile = path.join(allowedDir, 'temp.txt'); + const targetSymlink = path.join(allowedDir, 'target-symlink.txt'); + const forbiddenTarget = path.join(forbiddenDir, 'forbidden-target.txt'); + + // Create forbidden target + await fs.writeFile(forbiddenTarget, 'ORIGINAL CONTENT', 'utf-8'); + + // Create symlink pointing to forbidden location + await fs.symlink(forbiddenTarget, targetSymlink); + + // Write temp file + await fs.writeFile(tempFile, 'NEW CONTENT', 'utf-8'); + + // Rename temp file to symlink path + await fs.rename(tempFile, targetSymlink); + + // Check what happened + const symlinkExists = await fs.lstat(targetSymlink).then(() => true).catch(() => false); + const isSymlink = symlinkExists && (await fs.lstat(targetSymlink)).isSymbolicLink(); + const targetContent = await fs.readFile(targetSymlink, 'utf-8'); + const forbiddenContent = await fs.readFile(forbiddenTarget, 'utf-8'); + + // Rename should replace the symlink with a regular file + expect(isSymlink).toBe(false); + expect(targetContent).toBe('NEW CONTENT'); + expect(forbiddenContent).toBe('ORIGINAL CONTENT'); // Unchanged + }); + }); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/roots-utils.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/roots-utils.test.ts new file mode 100644 index 00000000..1a394839 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/roots-utils.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { getValidRootDirectories } from '../roots-utils.js'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import type { Root } from '@modelcontextprotocol/sdk/types.js'; + +describe('getValidRootDirectories', () => { + let testDir1: string; + let testDir2: string; + let testDir3: string; + let testFile: string; + + beforeEach(() => { + // Create test directories + testDir1 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test1-'))); + testDir2 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test2-'))); + testDir3 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test3-'))); + + // Create a test file (not a directory) + testFile = join(testDir1, 'test-file.txt'); + writeFileSync(testFile, 'test content'); + }); + + afterEach(() => { + // Cleanup + rmSync(testDir1, { recursive: true, force: true }); + rmSync(testDir2, { recursive: true, force: true }); + rmSync(testDir3, { recursive: true, force: true }); + }); + + describe('valid directory processing', () => { + it('should process all URI formats and edge cases', async () => { + const roots = [ + { uri: `file://${testDir1}`, name: 'File URI' }, + { uri: testDir2, name: 'Plain path' }, + { uri: testDir3 } // Plain path without name property + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toContain(testDir1); + expect(result).toContain(testDir2); + expect(result).toContain(testDir3); + expect(result).toHaveLength(3); + }); + + it('should normalize complex paths', async () => { + const subDir = join(testDir1, 'subdir'); + mkdirSync(subDir); + + const roots = [ + { uri: `file://${testDir1}/./subdir/../subdir`, name: 'Complex Path' } + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(subDir); + }); + }); + + describe('error handling', () => { + + it('should handle various error types', async () => { + const nonExistentDir = join(tmpdir(), 'non-existent-directory-12345'); + const invalidPath = '\0invalid\0path'; // Null bytes cause different error types + const roots = [ + { uri: `file://${testDir1}`, name: 'Valid Dir' }, + { uri: `file://${nonExistentDir}`, name: 'Non-existent Dir' }, + { uri: `file://${testFile}`, name: 'File Not Dir' }, + { uri: `file://${invalidPath}`, name: 'Invalid Path' } + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toContain(testDir1); + expect(result).not.toContain(nonExistentDir); + expect(result).not.toContain(testFile); + expect(result).not.toContain(invalidPath); + expect(result).toHaveLength(1); + }); + }); +}); \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/startup-validation.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/startup-validation.test.ts new file mode 100644 index 00000000..3be283df --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/startup-validation.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { spawn } from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs/promises'; +import * as os from 'os'; + +const SERVER_PATH = path.join(__dirname, '..', 'dist', 'index.js'); + +/** + * Spawns the filesystem server with given arguments and returns exit info + */ +async function spawnServer(args: string[], timeoutMs = 2000): Promise<{ exitCode: number | null; stderr: string }> { + return new Promise((resolve) => { + const proc = spawn('node', [SERVER_PATH, ...args], { + stdio: ['pipe', 'pipe', 'pipe'], + }); + + let stderr = ''; + proc.stderr?.on('data', (data) => { + stderr += data.toString(); + }); + + const timeout = setTimeout(() => { + proc.kill('SIGTERM'); + }, timeoutMs); + + proc.on('close', (code) => { + clearTimeout(timeout); + resolve({ exitCode: code, stderr }); + }); + + proc.on('error', (err) => { + clearTimeout(timeout); + resolve({ exitCode: 1, stderr: err.message }); + }); + }); +} + +describe('Startup Directory Validation', () => { + let testDir: string; + let accessibleDir: string; + let accessibleDir2: string; + + beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-startup-test-')); + accessibleDir = path.join(testDir, 'accessible'); + accessibleDir2 = path.join(testDir, 'accessible2'); + await fs.mkdir(accessibleDir, { recursive: true }); + await fs.mkdir(accessibleDir2, { recursive: true }); + }); + + afterEach(async () => { + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('should start successfully with all accessible directories', async () => { + const result = await spawnServer([accessibleDir, accessibleDir2]); + // Server starts and runs (we kill it after timeout, so exit code is null or from SIGTERM) + expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio'); + expect(result.stderr).not.toContain('Error:'); + }); + + it('should skip inaccessible directory and continue with accessible one', async () => { + const nonExistentDir = path.join(testDir, 'non-existent-dir-12345'); + + const result = await spawnServer([nonExistentDir, accessibleDir]); + + // Should warn about inaccessible directory + expect(result.stderr).toContain('Warning: Cannot access directory'); + expect(result.stderr).toContain(nonExistentDir); + + // Should still start successfully + expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio'); + }); + + it('should exit with error when ALL directories are inaccessible', async () => { + const nonExistent1 = path.join(testDir, 'non-existent-1'); + const nonExistent2 = path.join(testDir, 'non-existent-2'); + + const result = await spawnServer([nonExistent1, nonExistent2]); + + // Should exit with error + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Error: None of the specified directories are accessible'); + }); + + it('should warn when path is not a directory', async () => { + const filePath = path.join(testDir, 'not-a-directory.txt'); + await fs.writeFile(filePath, 'content'); + + const result = await spawnServer([filePath, accessibleDir]); + + // Should warn about non-directory + expect(result.stderr).toContain('Warning:'); + expect(result.stderr).toContain('not a directory'); + + // Should still start with the valid directory + expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio'); + }); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/structured-content.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/structured-content.test.ts new file mode 100644 index 00000000..4b8f92b0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/structured-content.test.ts @@ -0,0 +1,158 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { spawn } from 'child_process'; + +/** + * Integration tests to verify that tool handlers return structuredContent + * that matches the declared outputSchema. + * + * These tests address issues #3110, #3106, #3093 where tools were returning + * structuredContent: { content: [contentBlock] } (array) instead of + * structuredContent: { content: string } as declared in outputSchema. + */ +describe('structuredContent schema compliance', () => { + let client: Client; + let transport: StdioClientTransport; + let testDir: string; + + beforeEach(async () => { + // Create a temp directory for testing + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-test-')); + + // Create test files + await fs.writeFile(path.join(testDir, 'test.txt'), 'test content'); + await fs.mkdir(path.join(testDir, 'subdir')); + await fs.writeFile(path.join(testDir, 'subdir', 'nested.txt'), 'nested content'); + + // Start the MCP server + const serverPath = path.resolve(__dirname, '../dist/index.js'); + transport = new StdioClientTransport({ + command: 'node', + args: [serverPath, testDir], + }); + + client = new Client({ + name: 'test-client', + version: '1.0.0', + }, { + capabilities: {} + }); + + await client.connect(transport); + }); + + afterEach(async () => { + await client?.close(); + await fs.rm(testDir, { recursive: true, force: true }); + }); + + describe('directory_tree', () => { + it('should return structuredContent.content as a string, not an array', async () => { + const result = await client.callTool({ + name: 'directory_tree', + arguments: { path: testDir } + }); + + // The result should have structuredContent + expect(result.structuredContent).toBeDefined(); + + // structuredContent.content should be a string (matching outputSchema: { content: z.string() }) + const structuredContent = result.structuredContent as { content: unknown }; + expect(typeof structuredContent.content).toBe('string'); + + // It should NOT be an array + expect(Array.isArray(structuredContent.content)).toBe(false); + + // The content should be valid JSON representing the tree + const treeData = JSON.parse(structuredContent.content as string); + expect(Array.isArray(treeData)).toBe(true); + }); + }); + + describe('list_directory_with_sizes', () => { + it('should return structuredContent.content as a string, not an array', async () => { + const result = await client.callTool({ + name: 'list_directory_with_sizes', + arguments: { path: testDir } + }); + + // The result should have structuredContent + expect(result.structuredContent).toBeDefined(); + + // structuredContent.content should be a string (matching outputSchema: { content: z.string() }) + const structuredContent = result.structuredContent as { content: unknown }; + expect(typeof structuredContent.content).toBe('string'); + + // It should NOT be an array + expect(Array.isArray(structuredContent.content)).toBe(false); + + // The content should contain directory listing info + expect(structuredContent.content).toContain('[FILE]'); + }); + }); + + describe('move_file', () => { + it('should return structuredContent.content as a string, not an array', async () => { + const sourcePath = path.join(testDir, 'test.txt'); + const destPath = path.join(testDir, 'moved.txt'); + + const result = await client.callTool({ + name: 'move_file', + arguments: { + source: sourcePath, + destination: destPath + } + }); + + // The result should have structuredContent + expect(result.structuredContent).toBeDefined(); + + // structuredContent.content should be a string (matching outputSchema: { content: z.string() }) + const structuredContent = result.structuredContent as { content: unknown }; + expect(typeof structuredContent.content).toBe('string'); + + // It should NOT be an array + expect(Array.isArray(structuredContent.content)).toBe(false); + + // The content should contain success message + expect(structuredContent.content).toContain('Successfully moved'); + }); + }); + + describe('list_directory (control - already working)', () => { + it('should return structuredContent.content as a string', async () => { + const result = await client.callTool({ + name: 'list_directory', + arguments: { path: testDir } + }); + + expect(result.structuredContent).toBeDefined(); + + const structuredContent = result.structuredContent as { content: unknown }; + expect(typeof structuredContent.content).toBe('string'); + expect(Array.isArray(structuredContent.content)).toBe(false); + }); + }); + + describe('search_files (control - already working)', () => { + it('should return structuredContent.content as a string', async () => { + const result = await client.callTool({ + name: 'search_files', + arguments: { + path: testDir, + pattern: '*.txt' + } + }); + + expect(result.structuredContent).toBeDefined(); + + const structuredContent = result.structuredContent as { content: unknown }; + expect(typeof structuredContent.content).toBe('string'); + expect(Array.isArray(structuredContent.content)).toBe(false); + }); + }); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/index.ts new file mode 100644 index 00000000..7b67e63e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/index.ts @@ -0,0 +1,767 @@ +#!/usr/bin/env node + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolResult, + RootsListChangedNotificationSchema, + type Root, +} from "@modelcontextprotocol/sdk/types.js"; +import fs from "fs/promises"; +import { createReadStream } from "fs"; +import path from "path"; +import { z } from "zod"; +import { minimatch } from "minimatch"; +import { normalizePath, expandHome } from './path-utils.js'; +import { getValidRootDirectories } from './roots-utils.js'; +import { + // Function imports + formatSize, + validatePath, + getFileStats, + readFileContent, + writeFileContent, + searchFilesWithValidation, + applyFileEdits, + tailFile, + headFile, + setAllowedDirectories, +} from './lib.js'; + +// Command line argument parsing +const args = process.argv.slice(2); +if (args.length === 0) { + console.error("Usage: mcp-server-filesystem [allowed-directory] [additional-directories...]"); + console.error("Note: Allowed directories can be provided via:"); + console.error(" 1. Command-line arguments (shown above)"); + console.error(" 2. MCP roots protocol (if client supports it)"); + console.error("At least one directory must be provided by EITHER method for the server to operate."); +} + +// Store allowed directories in normalized and resolved form +// We store BOTH the original path AND the resolved path to handle symlinks correctly +// This fixes the macOS /tmp -> /private/tmp symlink issue where users specify /tmp +// but the resolved path is /private/tmp +let allowedDirectories = (await Promise.all( + args.map(async (dir) => { + const expanded = expandHome(dir); + const absolute = path.resolve(expanded); + const normalizedOriginal = normalizePath(absolute); + try { + // Security: Resolve symlinks in allowed directories during startup + // This ensures we know the real paths and can validate against them later + const resolved = await fs.realpath(absolute); + const normalizedResolved = normalizePath(resolved); + // Return both original and resolved paths if they differ + // This allows matching against either /tmp or /private/tmp on macOS + if (normalizedOriginal !== normalizedResolved) { + return [normalizedOriginal, normalizedResolved]; + } + return [normalizedResolved]; + } catch (error) { + // If we can't resolve (doesn't exist), use the normalized absolute path + // This allows configuring allowed dirs that will be created later + return [normalizedOriginal]; + } + }) +)).flat(); + +// Filter to only accessible directories, warn about inaccessible ones +const accessibleDirectories: string[] = []; +for (const dir of allowedDirectories) { + try { + const stats = await fs.stat(dir); + if (stats.isDirectory()) { + accessibleDirectories.push(dir); + } else { + console.error(`Warning: ${dir} is not a directory, skipping`); + } + } catch (error) { + console.error(`Warning: Cannot access directory ${dir}, skipping`); + } +} + +// Exit only if ALL paths are inaccessible (and some were specified) +if (accessibleDirectories.length === 0 && allowedDirectories.length > 0) { + console.error("Error: None of the specified directories are accessible"); + process.exit(1); +} + +allowedDirectories = accessibleDirectories; + +// Initialize the global allowedDirectories in lib.ts +setAllowedDirectories(allowedDirectories); + +// Schema definitions +const ReadTextFileArgsSchema = z.object({ + path: z.string(), + tail: z.number().optional().describe('If provided, returns only the last N lines of the file'), + head: z.number().optional().describe('If provided, returns only the first N lines of the file') +}); + +const ReadMediaFileArgsSchema = z.object({ + path: z.string() +}); + +const ReadMultipleFilesArgsSchema = z.object({ + paths: z + .array(z.string()) + .min(1, "At least one file path must be provided") + .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories."), +}); + +const WriteFileArgsSchema = z.object({ + path: z.string(), + content: z.string(), +}); + +const EditOperation = z.object({ + oldText: z.string().describe('Text to search for - must match exactly'), + newText: z.string().describe('Text to replace with') +}); + +const EditFileArgsSchema = z.object({ + path: z.string(), + edits: z.array(EditOperation), + dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format') +}); + +const CreateDirectoryArgsSchema = z.object({ + path: z.string(), +}); + +const ListDirectoryArgsSchema = z.object({ + path: z.string(), +}); + +const ListDirectoryWithSizesArgsSchema = z.object({ + path: z.string(), + sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'), +}); + +const DirectoryTreeArgsSchema = z.object({ + path: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) +}); + +const MoveFileArgsSchema = z.object({ + source: z.string(), + destination: z.string(), +}); + +const SearchFilesArgsSchema = z.object({ + path: z.string(), + pattern: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) +}); + +const GetFileInfoArgsSchema = z.object({ + path: z.string(), +}); + +// Server setup +const server = new McpServer( + { + name: "secure-filesystem-server", + version: "0.2.0", + } +); + +// Reads a file as a stream of buffers, concatenates them, and then encodes +// the result to a Base64 string. This is a memory-efficient way to handle +// binary data from a stream before the final encoding. +async function readFileAsBase64Stream(filePath: string): Promise { + return new Promise((resolve, reject) => { + const stream = createReadStream(filePath); + const chunks: Buffer[] = []; + stream.on('data', (chunk) => { + chunks.push(chunk as Buffer); + }); + stream.on('end', () => { + const finalBuffer = Buffer.concat(chunks); + resolve(finalBuffer.toString('base64')); + }); + stream.on('error', (err) => reject(err)); + }); +} + +// Tool registrations + +// read_file (deprecated) and read_text_file +const readTextFileHandler = async (args: z.infer) => { + const validPath = await validatePath(args.path); + + if (args.head && args.tail) { + throw new Error("Cannot specify both head and tail parameters simultaneously"); + } + + let content: string; + if (args.tail) { + content = await tailFile(validPath, args.tail); + } else if (args.head) { + content = await headFile(validPath, args.head); + } else { + content = await readFileContent(validPath); + } + + return { + content: [{ type: "text" as const, text: content }], + structuredContent: { content } + }; +}; + +server.registerTool( + "read_file", + { + title: "Read File (Deprecated)", + description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.", + inputSchema: ReadTextFileArgsSchema.shape, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + readTextFileHandler +); + +server.registerTool( + "read_text_file", + { + title: "Read Text File", + description: + "Read the complete contents of a file from the file system as text. " + + "Handles various text encodings and provides detailed error messages " + + "if the file cannot be read. Use this tool when you need to examine " + + "the contents of a single file. Use the 'head' parameter to read only " + + "the first N lines of a file, or the 'tail' parameter to read only " + + "the last N lines of a file. Operates on the file as text regardless of extension. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string(), + tail: z.number().optional().describe("If provided, returns only the last N lines of the file"), + head: z.number().optional().describe("If provided, returns only the first N lines of the file") + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + readTextFileHandler +); + +server.registerTool( + "read_media_file", + { + title: "Read Media File", + description: + "Read an image or audio file. Returns the base64 encoded data and MIME type. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string() + }, + outputSchema: { + content: z.array(z.object({ + type: z.enum(["image", "audio", "blob"]), + data: z.string(), + mimeType: z.string() + })) + }, + annotations: { readOnlyHint: true } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + const extension = path.extname(validPath).toLowerCase(); + const mimeTypes: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".bmp": "image/bmp", + ".svg": "image/svg+xml", + ".mp3": "audio/mpeg", + ".wav": "audio/wav", + ".ogg": "audio/ogg", + ".flac": "audio/flac", + }; + const mimeType = mimeTypes[extension] || "application/octet-stream"; + const data = await readFileAsBase64Stream(validPath); + + const type = mimeType.startsWith("image/") + ? "image" + : mimeType.startsWith("audio/") + ? "audio" + // Fallback for other binary types, not officially supported by the spec but has been used for some time + : "blob"; + const contentItem = { type: type as 'image' | 'audio' | 'blob', data, mimeType }; + return { + content: [contentItem], + structuredContent: { content: [contentItem] } + } as unknown as CallToolResult; + } +); + +server.registerTool( + "read_multiple_files", + { + title: "Read Multiple Files", + description: + "Read the contents of multiple files simultaneously. This is more " + + "efficient than reading files one by one when you need to analyze " + + "or compare multiple files. Each file's content is returned with its " + + "path as a reference. Failed reads for individual files won't stop " + + "the entire operation. Only works within allowed directories.", + inputSchema: { + paths: z.array(z.string()) + .min(1) + .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.") + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + async (args: z.infer) => { + const results = await Promise.all( + args.paths.map(async (filePath: string) => { + try { + const validPath = await validatePath(filePath); + const content = await readFileContent(validPath); + return `${filePath}:\n${content}\n`; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return `${filePath}: Error - ${errorMessage}`; + } + }), + ); + const text = results.join("\n---\n"); + return { + content: [{ type: "text" as const, text }], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "write_file", + { + title: "Write File", + description: + "Create a new file or completely overwrite an existing file with new content. " + + "Use with caution as it will overwrite existing files without warning. " + + "Handles text content with proper encoding. Only works within allowed directories.", + inputSchema: { + path: z.string(), + content: z.string() + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + await writeFileContent(validPath, args.content); + const text = `Successfully wrote to ${args.path}`; + return { + content: [{ type: "text" as const, text }], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "edit_file", + { + title: "Edit File", + description: + "Make line-based edits to a text file. Each edit replaces exact line sequences " + + "with new content. Returns a git-style diff showing the changes made. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string(), + edits: z.array(z.object({ + oldText: z.string().describe("Text to search for - must match exactly"), + newText: z.string().describe("Text to replace with") + })), + dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + const result = await applyFileEdits(validPath, args.edits, args.dryRun); + return { + content: [{ type: "text" as const, text: result }], + structuredContent: { content: result } + }; + } +); + +server.registerTool( + "create_directory", + { + title: "Create Directory", + description: + "Create a new directory or ensure a directory exists. Can create multiple " + + "nested directories in one operation. If the directory already exists, " + + "this operation will succeed silently. Perfect for setting up directory " + + "structures for projects or ensuring required paths exist. Only works within allowed directories.", + inputSchema: { + path: z.string() + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + await fs.mkdir(validPath, { recursive: true }); + const text = `Successfully created directory ${args.path}`; + return { + content: [{ type: "text" as const, text }], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "list_directory", + { + title: "List Directory", + description: + "Get a detailed listing of all files and directories in a specified path. " + + "Results clearly distinguish between files and directories with [FILE] and [DIR] " + + "prefixes. This tool is essential for understanding directory structure and " + + "finding specific files within a directory. Only works within allowed directories.", + inputSchema: { + path: z.string() + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + const formatted = entries + .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`) + .join("\n"); + return { + content: [{ type: "text" as const, text: formatted }], + structuredContent: { content: formatted } + }; + } +); + +server.registerTool( + "list_directory_with_sizes", + { + title: "List Directory with Sizes", + description: + "Get a detailed listing of all files and directories in a specified path, including sizes. " + + "Results clearly distinguish between files and directories with [FILE] and [DIR] " + + "prefixes. This tool is useful for understanding directory structure and " + + "finding specific files within a directory. Only works within allowed directories.", + inputSchema: { + path: z.string(), + sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size") + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + + // Get detailed information for each entry + const detailedEntries = await Promise.all( + entries.map(async (entry) => { + const entryPath = path.join(validPath, entry.name); + try { + const stats = await fs.stat(entryPath); + return { + name: entry.name, + isDirectory: entry.isDirectory(), + size: stats.size, + mtime: stats.mtime + }; + } catch (error) { + return { + name: entry.name, + isDirectory: entry.isDirectory(), + size: 0, + mtime: new Date(0) + }; + } + }) + ); + + // Sort entries based on sortBy parameter + const sortedEntries = [...detailedEntries].sort((a, b) => { + if (args.sortBy === 'size') { + return b.size - a.size; // Descending by size + } + // Default sort by name + return a.name.localeCompare(b.name); + }); + + // Format the output + const formattedEntries = sortedEntries.map(entry => + `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${ + entry.isDirectory ? "" : formatSize(entry.size).padStart(10) + }` + ); + + // Add summary + const totalFiles = detailedEntries.filter(e => !e.isDirectory).length; + const totalDirs = detailedEntries.filter(e => e.isDirectory).length; + const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0); + + const summary = [ + "", + `Total: ${totalFiles} files, ${totalDirs} directories`, + `Combined size: ${formatSize(totalSize)}` + ]; + + const text = [...formattedEntries, ...summary].join("\n"); + const contentBlock = { type: "text" as const, text }; + return { + content: [contentBlock], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "directory_tree", + { + title: "Directory Tree", + description: + "Get a recursive tree view of files and directories as a JSON structure. " + + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + + "Files have no children array, while directories always have a children array (which may be empty). " + + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", + inputSchema: { + path: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + async (args: z.infer) => { + interface TreeEntry { + name: string; + type: 'file' | 'directory'; + children?: TreeEntry[]; + } + const rootPath = args.path; + + async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise { + const validPath = await validatePath(currentPath); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + const result: TreeEntry[] = []; + + for (const entry of entries) { + const relativePath = path.relative(rootPath, path.join(currentPath, entry.name)); + const shouldExclude = excludePatterns.some(pattern => { + if (pattern.includes('*')) { + return minimatch(relativePath, pattern, { dot: true }); + } + // For files: match exact name or as part of path + // For directories: match as directory path + return minimatch(relativePath, pattern, { dot: true }) || + minimatch(relativePath, `**/${pattern}`, { dot: true }) || + minimatch(relativePath, `**/${pattern}/**`, { dot: true }); + }); + if (shouldExclude) + continue; + + const entryData: TreeEntry = { + name: entry.name, + type: entry.isDirectory() ? 'directory' : 'file' + }; + + if (entry.isDirectory()) { + const subPath = path.join(currentPath, entry.name); + entryData.children = await buildTree(subPath, excludePatterns); + } + + result.push(entryData); + } + + return result; + } + + const treeData = await buildTree(rootPath, args.excludePatterns); + const text = JSON.stringify(treeData, null, 2); + const contentBlock = { type: "text" as const, text }; + return { + content: [contentBlock], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "move_file", + { + title: "Move File", + description: + "Move or rename files and directories. Can move files between directories " + + "and rename them in a single operation. If the destination exists, the " + + "operation will fail. Works across different directories and can be used " + + "for simple renaming within the same directory. Both source and destination must be within allowed directories.", + inputSchema: { + source: z.string(), + destination: z.string() + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } + }, + async (args: z.infer) => { + const validSourcePath = await validatePath(args.source); + const validDestPath = await validatePath(args.destination); + await fs.rename(validSourcePath, validDestPath); + const text = `Successfully moved ${args.source} to ${args.destination}`; + const contentBlock = { type: "text" as const, text }; + return { + content: [contentBlock], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "search_files", + { + title: "Search Files", + description: + "Recursively search for files and directories matching a pattern. " + + "The patterns should be glob-style patterns that match paths relative to the working directory. " + + "Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " + + "Returns full paths to all matching items. Great for finding files when you don't know their exact location. " + + "Only searches within allowed directories.", + inputSchema: { + path: z.string(), + pattern: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + const results = await searchFilesWithValidation(validPath, args.pattern, allowedDirectories, { excludePatterns: args.excludePatterns }); + const text = results.length > 0 ? results.join("\n") : "No matches found"; + return { + content: [{ type: "text" as const, text }], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "get_file_info", + { + title: "Get File Info", + description: + "Retrieve detailed metadata about a file or directory. Returns comprehensive " + + "information including size, creation time, last modified time, permissions, " + + "and type. This tool is perfect for understanding file characteristics " + + "without reading the actual content. Only works within allowed directories.", + inputSchema: { + path: z.string() + }, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + async (args: z.infer) => { + const validPath = await validatePath(args.path); + const info = await getFileStats(validPath); + const text = Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .join("\n"); + return { + content: [{ type: "text" as const, text }], + structuredContent: { content: text } + }; + } +); + +server.registerTool( + "list_allowed_directories", + { + title: "List Allowed Directories", + description: + "Returns the list of directories that this server is allowed to access. " + + "Subdirectories within these allowed directories are also accessible. " + + "Use this to understand which directories and their nested paths are available " + + "before trying to access files.", + inputSchema: {}, + outputSchema: { content: z.string() }, + annotations: { readOnlyHint: true } + }, + async () => { + const text = `Allowed directories:\n${allowedDirectories.join('\n')}`; + return { + content: [{ type: "text" as const, text }], + structuredContent: { content: text } + }; + } +); + +// Updates allowed directories based on MCP client roots +async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) { + const validatedRootDirs = await getValidRootDirectories(requestedRoots); + if (validatedRootDirs.length > 0) { + allowedDirectories = [...validatedRootDirs]; + setAllowedDirectories(allowedDirectories); // Update the global state in lib.ts + console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`); + } else { + console.error("No valid root directories provided by client"); + } +} + +// Handles dynamic roots updates during runtime, when client sends "roots/list_changed" notification, server fetches the updated roots and replaces all allowed directories with the new roots. +server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => { + try { + // Request the updated roots list from the client + const response = await server.server.listRoots(); + if (response && 'roots' in response) { + await updateAllowedDirectoriesFromRoots(response.roots); + } + } catch (error) { + console.error("Failed to request roots from client:", error instanceof Error ? error.message : String(error)); + } +}); + +// Handles post-initialization setup, specifically checking for and fetching MCP roots. +server.server.oninitialized = async () => { + const clientCapabilities = server.server.getClientCapabilities(); + + if (clientCapabilities?.roots) { + try { + const response = await server.server.listRoots(); + if (response && 'roots' in response) { + await updateAllowedDirectoriesFromRoots(response.roots); + } else { + console.error("Client returned no roots set, keeping current settings"); + } + } catch (error) { + console.error("Failed to request initial roots from client:", error instanceof Error ? error.message : String(error)); + } + } else { + if (allowedDirectories.length > 0) { + console.error("Client does not support MCP Roots, using allowed directories set from server args:", allowedDirectories); + }else{ + throw new Error(`Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`); + } + } +}; + +// Start server +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Secure MCP Filesystem Server running on stdio"); + if (allowedDirectories.length === 0) { + console.error("Started without allowed directories - waiting for client to provide roots via MCP protocol"); + } +} + +runServer().catch((error) => { + console.error("Fatal error running server:", error); + process.exit(1); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/lib.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/lib.ts new file mode 100644 index 00000000..17e4654c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/lib.ts @@ -0,0 +1,415 @@ +import fs from "fs/promises"; +import path from "path"; +import os from 'os'; +import { randomBytes } from 'crypto'; +import { diffLines, createTwoFilesPatch } from 'diff'; +import { minimatch } from 'minimatch'; +import { normalizePath, expandHome } from './path-utils.js'; +import { isPathWithinAllowedDirectories } from './path-validation.js'; + +// Global allowed directories - set by the main module +let allowedDirectories: string[] = []; + +// Function to set allowed directories from the main module +export function setAllowedDirectories(directories: string[]): void { + allowedDirectories = [...directories]; +} + +// Function to get current allowed directories +export function getAllowedDirectories(): string[] { + return [...allowedDirectories]; +} + +// Type definitions +interface FileInfo { + size: number; + created: Date; + modified: Date; + accessed: Date; + isDirectory: boolean; + isFile: boolean; + permissions: string; +} + +export interface SearchOptions { + excludePatterns?: string[]; +} + +export interface SearchResult { + path: string; + isDirectory: boolean; +} + +// Pure Utility Functions +export function formatSize(bytes: number): string { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) return '0 B'; + + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + + if (i < 0 || i === 0) return `${bytes} ${units[0]}`; + + const unitIndex = Math.min(i, units.length - 1); + return `${(bytes / Math.pow(1024, unitIndex)).toFixed(2)} ${units[unitIndex]}`; +} + +export function normalizeLineEndings(text: string): string { + return text.replace(/\r\n/g, '\n'); +} + +export function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string { + // Ensure consistent line endings for diff + const normalizedOriginal = normalizeLineEndings(originalContent); + const normalizedNew = normalizeLineEndings(newContent); + + return createTwoFilesPatch( + filepath, + filepath, + normalizedOriginal, + normalizedNew, + 'original', + 'modified' + ); +} + +// Helper function to resolve relative paths against allowed directories +function resolveRelativePathAgainstAllowedDirectories(relativePath: string): string { + if (allowedDirectories.length === 0) { + // Fallback to process.cwd() if no allowed directories are set + return path.resolve(process.cwd(), relativePath); + } + + // Try to resolve relative path against each allowed directory + for (const allowedDir of allowedDirectories) { + const candidate = path.resolve(allowedDir, relativePath); + const normalizedCandidate = normalizePath(candidate); + + // Check if the resulting path lies within any allowed directory + if (isPathWithinAllowedDirectories(normalizedCandidate, allowedDirectories)) { + return candidate; + } + } + + // If no valid resolution found, use the first allowed directory as base + // This provides a consistent fallback behavior + return path.resolve(allowedDirectories[0], relativePath); +} + +// Security & Validation Functions +export async function validatePath(requestedPath: string): Promise { + const expandedPath = expandHome(requestedPath); + const absolute = path.isAbsolute(expandedPath) + ? path.resolve(expandedPath) + : resolveRelativePathAgainstAllowedDirectories(expandedPath); + + const normalizedRequested = normalizePath(absolute); + + // Security: Check if path is within allowed directories before any file operations + const isAllowed = isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories); + if (!isAllowed) { + throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`); + } + + // Security: Handle symlinks by checking their real path to prevent symlink attacks + // This prevents attackers from creating symlinks that point outside allowed directories + try { + const realPath = await fs.realpath(absolute); + const normalizedReal = normalizePath(realPath); + if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirectories)) { + throw new Error(`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`); + } + return realPath; + } catch (error) { + // Security: For new files that don't exist yet, verify parent directory + // This ensures we can't create files in unauthorized locations + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + const parentDir = path.dirname(absolute); + try { + const realParentPath = await fs.realpath(parentDir); + const normalizedParent = normalizePath(realParentPath); + if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories)) { + throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`); + } + return absolute; + } catch { + throw new Error(`Parent directory does not exist: ${parentDir}`); + } + } + throw error; + } +} + + +// File Operations +export async function getFileStats(filePath: string): Promise { + const stats = await fs.stat(filePath); + return { + size: stats.size, + created: stats.birthtime, + modified: stats.mtime, + accessed: stats.atime, + isDirectory: stats.isDirectory(), + isFile: stats.isFile(), + permissions: stats.mode.toString(8).slice(-3), + }; +} + +export async function readFileContent(filePath: string, encoding: string = 'utf-8'): Promise { + return await fs.readFile(filePath, encoding as BufferEncoding); +} + +export async function writeFileContent(filePath: string, content: string): Promise { + try { + // Security: 'wx' flag ensures exclusive creation - fails if file/symlink exists, + // preventing writes through pre-existing symlinks + await fs.writeFile(filePath, content, { encoding: "utf-8", flag: 'wx' }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EEXIST') { + // Security: Use atomic rename to prevent race conditions where symlinks + // could be created between validation and write. Rename operations + // replace the target file atomically and don't follow symlinks. + const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`; + try { + await fs.writeFile(tempPath, content, 'utf-8'); + await fs.rename(tempPath, filePath); + } catch (renameError) { + try { + await fs.unlink(tempPath); + } catch {} + throw renameError; + } + } else { + throw error; + } + } +} + + +// File Editing Functions +interface FileEdit { + oldText: string; + newText: string; +} + +export async function applyFileEdits( + filePath: string, + edits: FileEdit[], + dryRun: boolean = false +): Promise { + // Read file content and normalize line endings + const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); + + // Apply edits sequentially + let modifiedContent = content; + for (const edit of edits) { + const normalizedOld = normalizeLineEndings(edit.oldText); + const normalizedNew = normalizeLineEndings(edit.newText); + + // If exact match exists, use it + if (modifiedContent.includes(normalizedOld)) { + modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); + continue; + } + + // Otherwise, try line-by-line matching with flexibility for whitespace + const oldLines = normalizedOld.split('\n'); + const contentLines = modifiedContent.split('\n'); + let matchFound = false; + + for (let i = 0; i <= contentLines.length - oldLines.length; i++) { + const potentialMatch = contentLines.slice(i, i + oldLines.length); + + // Compare lines with normalized whitespace + const isMatch = oldLines.every((oldLine, j) => { + const contentLine = potentialMatch[j]; + return oldLine.trim() === contentLine.trim(); + }); + + if (isMatch) { + // Preserve original indentation of first line + const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; + const newLines = normalizedNew.split('\n').map((line, j) => { + if (j === 0) return originalIndent + line.trimStart(); + // For subsequent lines, try to preserve relative indentation + const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''; + const newIndent = line.match(/^\s*/)?.[0] || ''; + if (oldIndent && newIndent) { + const relativeIndent = newIndent.length - oldIndent.length; + return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart(); + } + return line; + }); + + contentLines.splice(i, oldLines.length, ...newLines); + modifiedContent = contentLines.join('\n'); + matchFound = true; + break; + } + } + + if (!matchFound) { + throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); + } + } + + // Create unified diff + const diff = createUnifiedDiff(content, modifiedContent, filePath); + + // Format diff with appropriate number of backticks + let numBackticks = 3; + while (diff.includes('`'.repeat(numBackticks))) { + numBackticks++; + } + const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; + + if (!dryRun) { + // Security: Use atomic rename to prevent race conditions where symlinks + // could be created between validation and write. Rename operations + // replace the target file atomically and don't follow symlinks. + const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`; + try { + await fs.writeFile(tempPath, modifiedContent, 'utf-8'); + await fs.rename(tempPath, filePath); + } catch (error) { + try { + await fs.unlink(tempPath); + } catch {} + throw error; + } + } + + return formattedDiff; +} + +// Memory-efficient implementation to get the last N lines of a file +export async function tailFile(filePath: string, numLines: number): Promise { + const CHUNK_SIZE = 1024; // Read 1KB at a time + const stats = await fs.stat(filePath); + const fileSize = stats.size; + + if (fileSize === 0) return ''; + + // Open file for reading + const fileHandle = await fs.open(filePath, 'r'); + try { + const lines: string[] = []; + let position = fileSize; + let chunk = Buffer.alloc(CHUNK_SIZE); + let linesFound = 0; + let remainingText = ''; + + // Read chunks from the end of the file until we have enough lines + while (position > 0 && linesFound < numLines) { + const size = Math.min(CHUNK_SIZE, position); + position -= size; + + const { bytesRead } = await fileHandle.read(chunk, 0, size, position); + if (!bytesRead) break; + + // Get the chunk as a string and prepend any remaining text from previous iteration + const readData = chunk.slice(0, bytesRead).toString('utf-8'); + const chunkText = readData + remainingText; + + // Split by newlines and count + const chunkLines = normalizeLineEndings(chunkText).split('\n'); + + // If this isn't the end of the file, the first line is likely incomplete + // Save it to prepend to the next chunk + if (position > 0) { + remainingText = chunkLines[0]; + chunkLines.shift(); // Remove the first (incomplete) line + } + + // Add lines to our result (up to the number we need) + for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) { + lines.unshift(chunkLines[i]); + linesFound++; + } + } + + return lines.join('\n'); + } finally { + await fileHandle.close(); + } +} + +// New function to get the first N lines of a file +export async function headFile(filePath: string, numLines: number): Promise { + const fileHandle = await fs.open(filePath, 'r'); + try { + const lines: string[] = []; + let buffer = ''; + let bytesRead = 0; + const chunk = Buffer.alloc(1024); // 1KB buffer + + // Read chunks and count lines until we have enough or reach EOF + while (lines.length < numLines) { + const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead); + if (result.bytesRead === 0) break; // End of file + bytesRead += result.bytesRead; + buffer += chunk.slice(0, result.bytesRead).toString('utf-8'); + + const newLineIndex = buffer.lastIndexOf('\n'); + if (newLineIndex !== -1) { + const completeLines = buffer.slice(0, newLineIndex).split('\n'); + buffer = buffer.slice(newLineIndex + 1); + for (const line of completeLines) { + lines.push(line); + if (lines.length >= numLines) break; + } + } + } + + // If there is leftover content and we still need lines, add it + if (buffer.length > 0 && lines.length < numLines) { + lines.push(buffer); + } + + return lines.join('\n'); + } finally { + await fileHandle.close(); + } +} + +export async function searchFilesWithValidation( + rootPath: string, + pattern: string, + allowedDirectories: string[], + options: SearchOptions = {} +): Promise { + const { excludePatterns = [] } = options; + const results: string[] = []; + + async function search(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + + try { + await validatePath(fullPath); + + const relativePath = path.relative(rootPath, fullPath); + const shouldExclude = excludePatterns.some(excludePattern => + minimatch(relativePath, excludePattern, { dot: true }) + ); + + if (shouldExclude) continue; + + // Use glob matching for the search pattern + if (minimatch(relativePath, pattern, { dot: true })) { + results.push(fullPath); + } + + if (entry.isDirectory()) { + await search(fullPath); + } + } catch { + continue; + } + } + } + + await search(rootPath); + return results; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/.package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/.package-lock.json new file mode 100644 index 00000000..da1e0b9e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/.package-lock.json @@ -0,0 +1,2798 @@ +{ + "name": "@modelcontextprotocol/server-filesystem", + "version": "0.6.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@types/diff": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", + "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", + "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shelljs/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/shelljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shelljs/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/README.md new file mode 100644 index 00000000..1463c9f6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/README.md @@ -0,0 +1,218 @@ +# @ampproject/remapping + +> Remap sequential sourcemaps through transformations to point at the original source code + +Remapping allows you to take the sourcemaps generated through transforming your code and "remap" +them to the original source locations. Think "my minified code, transformed with babel and bundled +with webpack", all pointing to the correct location in your original source code. + +With remapping, none of your source code transformations need to be aware of the input's sourcemap, +they only need to generate an output sourcemap. This greatly simplifies building custom +transformations (think a find-and-replace). + +## Installation + +```sh +npm install @ampproject/remapping +``` + +## Usage + +```typescript +function remapping( + map: SourceMap | SourceMap[], + loader: (file: string, ctx: LoaderContext) => (SourceMap | null | undefined), + options?: { excludeContent: boolean, decodedMappings: boolean } +): SourceMap; + +// LoaderContext gives the loader the importing sourcemap, tree depth, the ability to override the +// "source" location (where child sources are resolved relative to, or the location of original +// source), and the ability to override the "content" of an original source for inclusion in the +// output sourcemap. +type LoaderContext = { + readonly importer: string; + readonly depth: number; + source: string; + content: string | null | undefined; +} +``` + +`remapping` takes the final output sourcemap, and a `loader` function. For every source file pointer +in the sourcemap, the `loader` will be called with the resolved path. If the path itself represents +a transformed file (it has a sourcmap associated with it), then the `loader` should return that +sourcemap. If not, the path will be treated as an original, untransformed source code. + +```js +// Babel transformed "helloworld.js" into "transformed.js" +const transformedMap = JSON.stringify({ + file: 'transformed.js', + // 1st column of 2nd line of output file translates into the 1st source + // file, line 3, column 2 + mappings: ';CAEE', + sources: ['helloworld.js'], + version: 3, +}); + +// Uglify minified "transformed.js" into "transformed.min.js" +const minifiedTransformedMap = JSON.stringify({ + file: 'transformed.min.js', + // 0th column of 1st line of output file translates into the 1st source + // file, line 2, column 1. + mappings: 'AACC', + names: [], + sources: ['transformed.js'], + version: 3, +}); + +const remapped = remapping( + minifiedTransformedMap, + (file, ctx) => { + + // The "transformed.js" file is an transformed file. + if (file === 'transformed.js') { + // The root importer is empty. + console.assert(ctx.importer === ''); + // The depth in the sourcemap tree we're currently loading. + // The root `minifiedTransformedMap` is depth 0, and its source children are depth 1, etc. + console.assert(ctx.depth === 1); + + return transformedMap; + } + + // Loader will be called to load transformedMap's source file pointers as well. + console.assert(file === 'helloworld.js'); + // `transformed.js`'s sourcemap points into `helloworld.js`. + console.assert(ctx.importer === 'transformed.js'); + // This is a source child of `transformed`, which is a source child of `minifiedTransformedMap`. + console.assert(ctx.depth === 2); + return null; + } +); + +console.log(remapped); +// { +// file: 'transpiled.min.js', +// mappings: 'AAEE', +// sources: ['helloworld.js'], +// version: 3, +// }; +``` + +In this example, `loader` will be called twice: + +1. `"transformed.js"`, the first source file pointer in the `minifiedTransformedMap`. We return the + associated sourcemap for it (its a transformed file, after all) so that sourcemap locations can + be traced through it into the source files it represents. +2. `"helloworld.js"`, our original, unmodified source code. This file does not have a sourcemap, so + we return `null`. + +The `remapped` sourcemap now points from `transformed.min.js` into locations in `helloworld.js`. If +you were to read the `mappings`, it says "0th column of the first line output line points to the 1st +column of the 2nd line of the file `helloworld.js`". + +### Multiple transformations of a file + +As a convenience, if you have multiple single-source transformations of a file, you may pass an +array of sourcemap files in the order of most-recent transformation sourcemap first. Note that this +changes the `importer` and `depth` of each call to our loader. So our above example could have been +written as: + +```js +const remapped = remapping( + [minifiedTransformedMap, transformedMap], + () => null +); + +console.log(remapped); +// { +// file: 'transpiled.min.js', +// mappings: 'AAEE', +// sources: ['helloworld.js'], +// version: 3, +// }; +``` + +### Advanced control of the loading graph + +#### `source` + +The `source` property can overridden to any value to change the location of the current load. Eg, +for an original source file, it allows us to change the location to the original source regardless +of what the sourcemap source entry says. And for transformed files, it allows us to change the +relative resolving location for child sources of the loaded sourcemap. + +```js +const remapped = remapping( + minifiedTransformedMap, + (file, ctx) => { + + if (file === 'transformed.js') { + // We pretend the transformed.js file actually exists in the 'src/' directory. When the nested + // source files are loaded, they will now be relative to `src/`. + ctx.source = 'src/transformed.js'; + return transformedMap; + } + + console.assert(file === 'src/helloworld.js'); + // We could futher change the source of this original file, eg, to be inside a nested directory + // itself. This will be reflected in the remapped sourcemap. + ctx.source = 'src/nested/transformed.js'; + return null; + } +); + +console.log(remapped); +// { +// …, +// sources: ['src/nested/helloworld.js'], +// }; +``` + + +#### `content` + +The `content` property can be overridden when we encounter an original source file. Eg, this allows +you to manually provide the source content of the original file regardless of whether the +`sourcesContent` field is present in the parent sourcemap. It can also be set to `null` to remove +the source content. + +```js +const remapped = remapping( + minifiedTransformedMap, + (file, ctx) => { + + if (file === 'transformed.js') { + // transformedMap does not include a `sourcesContent` field, so usually the remapped sourcemap + // would not include any `sourcesContent` values. + return transformedMap; + } + + console.assert(file === 'helloworld.js'); + // We can read the file to provide the source content. + ctx.content = fs.readFileSync(file, 'utf8'); + return null; + } +); + +console.log(remapped); +// { +// …, +// sourcesContent: [ +// 'console.log("Hello world!")', +// ], +// }; +``` + +### Options + +#### excludeContent + +By default, `excludeContent` is `false`. Passing `{ excludeContent: true }` will exclude the +`sourcesContent` field from the returned sourcemap. This is mainly useful when you want to reduce +the size out the sourcemap. + +#### decodedMappings + +By default, `decodedMappings` is `false`. Passing `{ decodedMappings: true }` will leave the +`mappings` field in a [decoded state](https://github.com/rich-harris/sourcemap-codec) instead of +encoding into a VLQ string. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/package.json new file mode 100644 index 00000000..091224c6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/package.json @@ -0,0 +1,75 @@ +{ + "name": "@ampproject/remapping", + "version": "2.3.0", + "description": "Remap sequential sourcemaps through transformations to point at the original source code", + "keywords": [ + "source", + "map", + "remap" + ], + "main": "dist/remapping.umd.js", + "module": "dist/remapping.mjs", + "types": "dist/types/remapping.d.ts", + "exports": { + ".": [ + { + "types": "./dist/types/remapping.d.ts", + "browser": "./dist/remapping.umd.js", + "require": "./dist/remapping.umd.js", + "import": "./dist/remapping.mjs" + }, + "./dist/remapping.umd.js" + ], + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "author": "Justin Ridgewell ", + "repository": { + "type": "git", + "url": "git+https://github.com/ampproject/remapping.git" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=6.0.0" + }, + "scripts": { + "build": "run-s -n build:*", + "build:rollup": "rollup -c rollup.config.js", + "build:ts": "tsc --project tsconfig.build.json", + "lint": "run-s -n lint:*", + "lint:prettier": "npm run test:lint:prettier -- --write", + "lint:ts": "npm run test:lint:ts -- --fix", + "prebuild": "rm -rf dist", + "prepublishOnly": "npm run preversion", + "preversion": "run-s test build", + "test": "run-s -n test:lint test:only", + "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand", + "test:lint": "run-s -n test:lint:*", + "test:lint:prettier": "prettier --check '{src,test}/**/*.ts'", + "test:lint:ts": "eslint '{src,test}/**/*.ts'", + "test:only": "jest --coverage", + "test:watch": "jest --coverage --watch" + }, + "devDependencies": { + "@rollup/plugin-typescript": "8.3.2", + "@types/jest": "27.4.1", + "@typescript-eslint/eslint-plugin": "5.20.0", + "@typescript-eslint/parser": "5.20.0", + "eslint": "8.14.0", + "eslint-config-prettier": "8.5.0", + "jest": "27.5.1", + "jest-config": "27.5.1", + "npm-run-all": "4.1.5", + "prettier": "2.6.2", + "rollup": "2.70.2", + "ts-jest": "27.1.4", + "tslib": "2.4.0", + "typescript": "4.6.3" + }, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/LICENSE new file mode 100644 index 00000000..f31575ec --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/README.md new file mode 100644 index 00000000..771b4700 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/README.md @@ -0,0 +1,19 @@ +# @babel/helper-string-parser + +> A utility package to parse strings + +See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information. + +## Install + +Using npm: + +```sh +npm install --save @babel/helper-string-parser +``` + +or using yarn: + +```sh +yarn add @babel/helper-string-parser +``` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/package.json new file mode 100644 index 00000000..c4c86e4f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/package.json @@ -0,0 +1,31 @@ +{ + "name": "@babel/helper-string-parser", + "version": "7.27.1", + "description": "A utility package to parse strings", + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-helper-string-parser" + }, + "homepage": "https://babel.dev/docs/en/next/babel-helper-string-parser", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "./lib/index.js", + "devDependencies": { + "charcodes": "^0.2.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "author": "The Babel Team (https://babel.dev/team)", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "type": "commonjs" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/LICENSE new file mode 100644 index 00000000..f31575ec --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/README.md new file mode 100644 index 00000000..05c19e64 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/README.md @@ -0,0 +1,19 @@ +# @babel/helper-validator-identifier + +> Validate identifier/keywords name + +See our website [@babel/helper-validator-identifier](https://babeljs.io/docs/babel-helper-validator-identifier) for more information. + +## Install + +Using npm: + +```sh +npm install --save @babel/helper-validator-identifier +``` + +or using yarn: + +```sh +yarn add @babel/helper-validator-identifier +``` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/package.json new file mode 100644 index 00000000..1aea38db --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/package.json @@ -0,0 +1,31 @@ +{ + "name": "@babel/helper-validator-identifier", + "version": "7.28.5", + "description": "Validate identifier/keywords name", + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-helper-validator-identifier" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "./lib/index.js", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "devDependencies": { + "@unicode/unicode-17.0.0": "^1.6.10", + "charcodes": "^0.2.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "author": "The Babel Team (https://babel.dev/team)", + "type": "commonjs" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/CHANGELOG.md new file mode 100644 index 00000000..b3840ac8 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/CHANGELOG.md @@ -0,0 +1,1073 @@ +# Changelog + +> **Tags:** +> - :boom: [Breaking Change] +> - :eyeglasses: [Spec Compliance] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +> Semver Policy: https://github.com/babel/babel/tree/main/packages/babel-parser#semver + +_Note: Gaps between patch versions are faulty, broken or test releases._ + +See the [Babel Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) for the pre-6.8.0 version Changelog. + +## 6.17.1 (2017-05-10) + +### :bug: Bug Fix + * Fix typo in flow spread operator error (Brian Ng) + * Fixed invalid number literal parsing ([#473](https://github.com/babel/babylon/pull/473)) (Alex Kuzmenko) + * Fix number parser ([#433](https://github.com/babel/babylon/pull/433)) (Alex Kuzmenko) + * Ensure non pattern shorthand props are checked for reserved words ([#479](https://github.com/babel/babylon/pull/479)) (Brian Ng) + * Remove jsx context when parsing arrow functions ([#475](https://github.com/babel/babylon/pull/475)) (Brian Ng) + * Allow super in class properties ([#499](https://github.com/babel/babylon/pull/499)) (Brian Ng) + * Allow flow class field to be named constructor ([#510](https://github.com/babel/babylon/pull/510)) (Brian Ng) + +## 6.17.0 (2017-04-20) + +### :bug: Bug Fix + * Cherry-pick #418 to 6.x ([#476](https://github.com/babel/babylon/pull/476)) (Sebastian McKenzie) + * Add support for invalid escapes in tagged templates ([#274](https://github.com/babel/babylon/pull/274)) (Kevin Gibbons) + * Throw error if new.target is used outside of a function ([#402](https://github.com/babel/babylon/pull/402)) (Brian Ng) + * Fix parsing of class properties ([#351](https://github.com/babel/babylon/pull/351)) (Kevin Gibbons) + * Fix parsing yield with dynamicImport ([#383](https://github.com/babel/babylon/pull/383)) (Brian Ng) + * Ensure consistent start args for parseParenItem ([#386](https://github.com/babel/babylon/pull/386)) (Brian Ng) + +## 7.0.0-beta.8 (2017-04-04) + +### New Feature +* Add support for flow type spread (#418) (Conrad Buck) +* Allow statics in flow interfaces (#427) (Brian Ng) + +### Bug Fix +* Fix predicate attachment to match flow parser (#428) (Brian Ng) +* Add extra.raw back to JSXText and JSXAttribute (#344) (Alex Rattray) +* Fix rest parameters with array and objects (#424) (Brian Ng) +* Fix number parser (#433) (Alex Kuzmenko) + +### Docs +* Fix CONTRIBUTING.md [skip ci] (#432) (Alex Kuzmenko) + +### Internal +* Use babel-register script when running babel smoke tests (#442) (Brian Ng) + +## 7.0.0-beta.7 (2017-03-22) + +### Spec Compliance +* Remove babylon plugin for template revision since it's stage-4 (#426) (Henry Zhu) + +### Bug Fix + +* Fix push-pop logic in flow (#405) (Daniel Tschinder) + +## 7.0.0-beta.6 (2017-03-21) + +### New Feature +* Add support for invalid escapes in tagged templates (#274) (Kevin Gibbons) + +### Polish +* Improves error message when super is called outside of constructor (#408) (Arshabh Kumar Agarwal) + +### Docs + +* [7.0] Moved value field in spec from ObjectMember to ObjectProperty as ObjectMethod's don't have it (#415) [skip ci] (James Browning) + +## 7.0.0-beta.5 (2017-03-21) + +### Bug Fix +* Throw error if new.target is used outside of a function (#402) (Brian Ng) +* Fix parsing of class properties (#351) (Kevin Gibbons) + +### Other + * Test runner: Detect extra property in 'actual' but not in 'expected'. (#407) (Andy) + * Optimize travis builds (#419) (Daniel Tschinder) + * Update codecov to 2.0 (#412) (Daniel Tschinder) + * Fix spec for ClassMethod: It doesn't have a function, it *is* a function. (#406) [skip ci] (Andy) + * Changed Non-existent RestPattern to RestElement which is what is actually parsed (#409) [skip ci] (James Browning) + * Upgrade flow to 0.41 (Daniel Tschinder) + * Fix watch command (#403) (Brian Ng) + * Update yarn lock (Daniel Tschinder) + * Fix watch command (#403) (Brian Ng) + * chore(package): update flow-bin to version 0.41.0 (#395) (greenkeeper[bot]) + * Add estree test for correct order of directives (Daniel Tschinder) + * Add DoExpression to spec (#364) (Alex Kuzmenko) + * Mention cloning of repository in CONTRIBUTING.md (#391) [skip ci] (Sumedh Nimkarde) + * Explain how to run only one test (#389) [skip ci] (Aaron Ang) + + ## 7.0.0-beta.4 (2017-03-01) + +* Don't consume async when checking for async func decl (#377) (Brian Ng) +* add `ranges` option [skip ci] (Henry Zhu) +* Don't parse class properties without initializers when classProperties is disabled and Flow is enabled (#300) (Andrew Levine) + +## 7.0.0-beta.3 (2017-02-28) + +- [7.0] Change RestProperty/SpreadProperty to RestElement/SpreadElement (#384) +- Merge changes from 6.x + +## 7.0.0-beta.2 (2017-02-20) + +- estree: correctly change literals in all cases (#368) (Daniel Tschinder) + +## 7.0.0-beta.1 (2017-02-20) + +- Fix negative number literal typeannotations (#366) (Daniel Tschinder) +- Update contributing with more test info [skip ci] (#355) (Brian Ng) + +## 7.0.0-beta.0 (2017-02-15) + +- Reintroduce Variance node (#333) (Daniel Tschinder) +- Rename NumericLiteralTypeAnnotation to NumberLiteralTypeAnnotation (#332) (Charles Pick) +- [7.0] Remove ForAwaitStatement, add await flag to ForOfStatement (#349) (Brandon Dail) +- chore(package): update ava to version 0.18.0 (#345) (greenkeeper[bot]) +- chore(package): update babel-plugin-istanbul to version 4.0.0 (#350) (greenkeeper[bot]) +- Change location of ObjectTypeIndexer to match flow (#228) (Daniel Tschinder) +- Rename flow AST Type ExistentialTypeParam to ExistsTypeAnnotation (#322) (Toru Kobayashi) +- Revert "Temporary rollback for erroring on trailing comma with spread (#154)" (#290) (Daniel Tschinder) +- Remove classConstructorCall plugin (#291) (Brian Ng) +- Update yarn.lock (Daniel Tschinder) +- Update cross-env to 3.x (Daniel Tschinder) +- [7.0] Remove node 0.10, 0.12 and 5 from Travis (#284) (Sergey Rubanov) +- Remove `String.fromCodePoint` shim (#279) (Mathias Bynens) + +## 6.16.1 (2017-02-23) + +### :bug: Regression + +- Revert "Fix export default async function to be FunctionDeclaration" ([#375](https://github.com/babel/babylon/pull/375)) + +Need to modify Babel for this AST node change, so moving to 7.0. + +- Revert "Don't parse class properties without initializers when classProperties plugin is disabled, and Flow is enabled" ([#376](https://github.com/babel/babylon/pull/376)) + +[react-native](https://github.com/facebook/react-native/issues/12542) broke with this so we reverted. + +## 6.16.0 (2017-02-23) + +### :rocket: New Feature + +***ESTree*** compatibility as plugin ([#277](https://github.com/babel/babylon/pull/277)) (Daniel Tschinder) + +We finally introduce a new compatibility layer for ESTree. To put babylon into ESTree-compatible mode the new plugin `estree` can be enabled. In this mode the parser will output an AST that is compliant to the specs of [ESTree](https://github.com/estree/estree/) + +We highly recommend everyone who uses babylon outside of babel to use this plugin. This will make it much easier for users to switch between different ESTree-compatible parsers. We so far tested several projects with different parsers and exchanged their parser to babylon and in nearly all cases it worked out of the box. Some other estree-compatible parsers include `acorn`, `esprima`, `espree`, `flow-parser`, etc. + +To enable `estree` mode simply add the plugin in the config: +```json +{ + "plugins": [ "estree" ] +} +``` + +If you want to migrate your project from non-ESTree mode to ESTree, have a look at our [Readme](https://github.com/babel/babylon/#output), where all deviations are mentioned. + +Add a parseExpression public method ([#213](https://github.com/babel/babylon/pull/213)) (jeromew) + +Babylon exports a new function to parse a single expression + +```js +import { parseExpression } from 'babylon'; + +const ast = parseExpression('x || y && z', options); +``` + +The returned AST will only consist of the expression. The options are the same as for `parse()` + +Add startLine option ([#346](https://github.com/babel/babylon/pull/346)) (Raphael Mu) + +A new option was added to babylon allowing to change the initial linenumber for the first line which is usually `1`. +Changing this for example to `100` will make line `1` of the input source to be marked as line `100`, line `2` as `101`, line `3` as `102`, ... + +Function predicate declaration ([#103](https://github.com/babel/babylon/pull/103)) (Panagiotis Vekris) + +Added support for function predicates which flow introduced in version 0.33.0 + +```js +declare function is_number(x: mixed): boolean %checks(typeof x === "number"); +``` + +Allow imports in declare module ([#315](https://github.com/babel/babylon/pull/315)) (Daniel Tschinder) + +Added support for imports within module declarations which flow introduced in version 0.37.0 + +```js +declare module "C" { + import type { DT } from "D"; + declare export type CT = { D: DT }; +} +``` + +### :eyeglasses: Spec Compliance + +Forbid semicolons after decorators in classes ([#352](https://github.com/babel/babylon/pull/352)) (Kevin Gibbons) + +This example now correctly throws an error when there is a semicolon after the decorator: + +```js +class A { +@a; +foo(){} +} +``` + +Keywords are not allowed as local specifier ([#307](https://github.com/babel/babylon/pull/307)) (Daniel Tschinder) + +Using keywords in imports is not allowed anymore: + +```js +import { default } from "foo"; +import { a as debugger } from "foo"; +``` + +Do not allow overwritting of primitive types ([#314](https://github.com/babel/babylon/pull/314)) (Daniel Tschinder) + +In flow it is now forbidden to overwrite the primitive types `"any"`, `"mixed"`, `"empty"`, `"bool"`, `"boolean"`, `"number"`, `"string"`, `"void"` and `"null"` with your own type declaration. + +Disallow import type { type a } from … ([#305](https://github.com/babel/babylon/pull/305)) (Daniel Tschinder) + +The following code now correctly throws an error + +```js +import type { type a } from "foo"; +``` + +Don't parse class properties without initializers when classProperties is disabled and Flow is enabled ([#300](https://github.com/babel/babylon/pull/300)) (Andrew Levine) + +Ensure that you enable the `classProperties` plugin in order to enable correct parsing of class properties. Prior to this version it was possible to parse them by enabling the `flow` plugin but this was not intended the behaviour. + +If you enable the flow plugin you can only define the type of the class properties, but not initialize them. + +Fix export default async function to be FunctionDeclaration ([#324](https://github.com/babel/babylon/pull/324)) (Daniel Tschinder) + +Parsing the following code now returns a `FunctionDeclaration` AST node instead of `FunctionExpression`. + +```js +export default async function bar() {}; +``` + +### :nail_care: Polish + +Improve error message on attempt to destructure named import ([#288](https://github.com/babel/babylon/pull/288)) (Brian Ng) + +### :bug: Bug Fix + +Fix negative number literal typeannotations ([#366](https://github.com/babel/babylon/pull/366)) (Daniel Tschinder) + +Ensure takeDecorators is called on exported class ([#358](https://github.com/babel/babylon/pull/358)) (Brian Ng) + +ESTree: correctly change literals in all cases ([#368](https://github.com/babel/babylon/pull/368)) (Daniel Tschinder) + +Correctly convert RestProperty to Assignable ([#339](https://github.com/babel/babylon/pull/339)) (Daniel Tschinder) + +Fix #321 by allowing question marks in type params ([#338](https://github.com/babel/babylon/pull/338)) (Daniel Tschinder) + +Fix #336 by correctly setting arrow-param ([#337](https://github.com/babel/babylon/pull/337)) (Daniel Tschinder) + +Fix parse error when destructuring `set` with default value ([#317](https://github.com/babel/babylon/pull/317)) (Brian Ng) + +Fix ObjectTypeCallProperty static ([#298](https://github.com/babel/babylon/pull/298)) (Dan Harper) + + +### :house: Internal + +Fix generator-method-with-computed-name spec ([#360](https://github.com/babel/babylon/pull/360)) (Alex Rattray) + +Fix flow type-parameter-declaration test with unintended semantic ([#361](https://github.com/babel/babylon/pull/361)) (Alex Rattray) + +Cleanup and splitup parser functions ([#295](https://github.com/babel/babylon/pull/295)) (Daniel Tschinder) + +chore(package): update flow-bin to version 0.38.0 ([#313](https://github.com/babel/babylon/pull/313)) (greenkeeper[bot]) + +Call inner function instead of 1:1 copy to plugin ([#294](https://github.com/babel/babylon/pull/294)) (Daniel Tschinder) + +Update eslint-config-babel to the latest version 🚀 ([#299](https://github.com/babel/babylon/pull/299)) (greenkeeper[bot]) + +Update eslint-config-babel to the latest version 🚀 ([#293](https://github.com/babel/babylon/pull/293)) (greenkeeper[bot]) + +devDeps: remove eslint-plugin-babel ([#292](https://github.com/babel/babylon/pull/292)) (Kai Cataldo) + +Correct indent eslint rule config ([#276](https://github.com/babel/babylon/pull/276)) (Daniel Tschinder) + +Fail tests that have expected.json and throws-option ([#285](https://github.com/babel/babylon/pull/285)) (Daniel Tschinder) + +### :memo: Documentation + +Update contributing with more test info [skip ci] ([#355](https://github.com/babel/babylon/pull/355)) (Brian Ng) + +Update API documentation ([#330](https://github.com/babel/babylon/pull/330)) (Timothy Gu) + +Added keywords to package.json ([#323](https://github.com/babel/babylon/pull/323)) (Dmytro) + +AST spec: fix casing of `RegExpLiteral` ([#318](https://github.com/babel/babylon/pull/318)) (Mathias Bynens) + +## 6.15.0 (2017-01-10) + +### :eyeglasses: Spec Compliance + +Add support for Flow shorthand import type ([#267](https://github.com/babel/babylon/pull/267)) (Jeff Morrison) + +This change implements flows new shorthand import syntax +and where previously you had to write this code: + +```js +import {someValue} from "blah"; +import type {someType} from "blah"; +import typeof {someOtherValue} from "blah"; +``` + +you can now write it like this: + +```js +import { + someValue, + type someType, + typeof someOtherValue, +} from "blah"; +``` + +For more information look at [this](https://github.com/facebook/flow/pull/2890) pull request. + +flow: allow leading pipes in all positions ([#256](https://github.com/babel/babylon/pull/256)) (Vladimir Kurchatkin) + +This change now allows a leading pipe everywhere types can be used: +```js +var f = (x): | 1 | 2 => 1; +``` + +Throw error when exporting non-declaration ([#241](https://github.com/babel/babylon/pull/241)) (Kai Cataldo) + +Previously babylon parsed the following exports, although they are not valid: +```js +export typeof foo; +export new Foo(); +export function() {}; +export for (;;); +export while(foo); +``` + +### :bug: Bug Fix + +Don't set inType flag when parsing property names ([#266](https://github.com/babel/babylon/pull/266)) (Vladimir Kurchatkin) + +This fixes parsing of this case: + +```js +const map = { + [age <= 17] : 'Too young' +}; +``` + +Fix source location for JSXEmptyExpression nodes (fixes #248) ([#249](https://github.com/babel/babylon/pull/249)) (James Long) + +The following case produced an invalid AST +```js +
{/* foo */}
+``` + +Use fromCodePoint to convert high value unicode entities ([#243](https://github.com/babel/babylon/pull/243)) (Ryan Duffy) + +When high value unicode entities (e.g. 💩) were used in the input source code they are now correctly encoded in the resulting AST. + +Rename folder to avoid Windows-illegal characters ([#281](https://github.com/babel/babylon/pull/281)) (Ryan Plant) + +Allow this.state.clone() when parsing decorators ([#262](https://github.com/babel/babylon/pull/262)) (Alex Rattray) + +### :house: Internal + +User external-helpers ([#254](https://github.com/babel/babylon/pull/254)) (Daniel Tschinder) + +Add watch script for dev ([#234](https://github.com/babel/babylon/pull/234)) (Kai Cataldo) + +Freeze current plugins list for "*" option, and remove from README.md ([#245](https://github.com/babel/babylon/pull/245)) (Andrew Levine) + +Prepare tests for multiple fixture runners. ([#240](https://github.com/babel/babylon/pull/240)) (Daniel Tschinder) + +Add some test coverage for decorators stage-0 plugin ([#250](https://github.com/babel/babylon/pull/250)) (Andrew Levine) + +Refactor tokenizer types file ([#263](https://github.com/babel/babylon/pull/263)) (Sven SAULEAU) + +Update eslint-config-babel to the latest version 🚀 ([#273](https://github.com/babel/babylon/pull/273)) (greenkeeper[bot]) + +chore(package): update rollup to version 0.41.0 ([#272](https://github.com/babel/babylon/pull/272)) (greenkeeper[bot]) + +chore(package): update flow-bin to version 0.37.0 ([#255](https://github.com/babel/babylon/pull/255)) (greenkeeper[bot]) + +## 6.14.1 (2016-11-17) + +### :bug: Bug Fix + +Allow `"plugins": ["*"]` ([#229](https://github.com/babel/babylon/pull/229)) (Daniel Tschinder) + +```js +{ + "plugins": ["*"] +} +``` + +Will include all parser plugins instead of specifying each one individually. Useful for tools like babel-eslint, jscodeshift, and ast-explorer. + +## 6.14.0 (2016-11-16) + +### :eyeglasses: Spec Compliance + +Throw error for reserved words `enum` and `await` ([#195](https://github.com/babel/babylon/pull/195)) (Kai Cataldo) + +[11.6.2.2 Future Reserved Words](http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words) + +Babylon will throw for more reserved words such as `enum` or `await` (in strict mode). + +``` +class enum {} // throws +class await {} // throws in strict mode (module) +``` + +Optional names for function types and object type indexers ([#197](https://github.com/babel/babylon/pull/197)) (Gabe Levi) + +So where you used to have to write + +```js +type A = (x: string, y: boolean) => number; +type B = (z: string) => number; +type C = { [key: string]: number }; +``` + +you can now write (with flow 0.34.0) + +```js +type A = (string, boolean) => number; +type B = string => number; +type C = { [string]: number }; +``` + +Parse flow nested array type annotations like `number[][]` ([#219](https://github.com/babel/babylon/pull/219)) (Bernhard Häussner) + +Supports these form now of specifying array types: + +```js +var a: number[][][][]; +var b: string[][]; +``` + +### :bug: Bug Fix + +Correctly eat semicolon at the end of `DelcareModuleExports` ([#223](https://github.com/babel/babylon/pull/223)) (Daniel Tschinder) + +``` +declare module "foo" { declare module.exports: number } +declare module "foo" { declare module.exports: number; } // also allowed now +``` + +### :house: Internal + + * Count Babel tests towards Babylon code coverage ([#182](https://github.com/babel/babylon/pull/182)) (Moti Zilberman) + * Fix strange line endings ([#214](https://github.com/babel/babylon/pull/214)) (Thomas Grainger) + * Add node 7 (Daniel Tschinder) + * chore(package): update flow-bin to version 0.34.0 ([#204](https://github.com/babel/babylon/pull/204)) (Greenkeeper) + +## v6.13.1 (2016-10-26) + +### :nail_care: Polish + +- Use rollup for bundling to speed up startup time ([#190](https://github.com/babel/babylon/pull/190)) ([@drewml](https://github.com/DrewML)) + +```js +const babylon = require('babylon'); +const ast = babylon.parse('var foo = "lol";'); +``` + +With that test case, there was a ~95ms savings by removing the need for node to build/traverse the dependency graph. + +**Without bundling** +![image](https://cloud.githubusercontent.com/assets/5233399/19420264/3133497e-93ad-11e6-9a6a-2da59c4f5c13.png) + +**With bundling** +![image](https://cloud.githubusercontent.com/assets/5233399/19420267/388f556e-93ad-11e6-813e-7c5c396be322.png) + +- add clean command [skip ci] ([#201](https://github.com/babel/babylon/pull/201)) (Henry Zhu) +- add ForAwaitStatement (async generator already added) [skip ci] ([#196](https://github.com/babel/babylon/pull/196)) (Henry Zhu) + +## v6.13.0 (2016-10-21) + +### :eyeglasses: Spec Compliance + +Property variance type annotations for Flow plugin ([#161](https://github.com/babel/babylon/pull/161)) (Sam Goldman) + +> See https://flowtype.org/docs/variance.html for more information + +```js +type T = { +p: T }; +interface T { -p: T }; +declare class T { +[k:K]: V }; +class T { -[k:K]: V }; +class C2 { +p: T = e }; +``` + +Raise error on duplicate definition of __proto__ ([#183](https://github.com/babel/babylon/pull/183)) (Moti Zilberman) + +```js +({ __proto__: 1, __proto__: 2 }) // Throws an error now +``` + +### :bug: Bug Fix + +Flow: Allow class properties to be named `static` ([#184](https://github.com/babel/babylon/pull/184)) (Moti Zilberman) + +```js +declare class A { + static: T; +} +``` + +Allow "async" as identifier for object literal property shorthand ([#187](https://github.com/babel/babylon/pull/187)) (Andrew Levine) + +```js +var foo = { async, bar }; +``` + +### :nail_care: Polish + +Fix flowtype and add inType to state ([#189](https://github.com/babel/babylon/pull/189)) (Daniel Tschinder) + +> This improves the performance slightly (because of hidden classes) + +### :house: Internal + +Fix .gitattributes line ending setting ([#191](https://github.com/babel/babylon/pull/191)) (Moti Zilberman) + +Increase test coverage ([#175](https://github.com/babel/babylon/pull/175) (Moti Zilberman) + +Readd missin .eslinignore for IDEs (Daniel Tschinder) + +Error on missing expected.json fixture in CI ([#188](https://github.com/babel/babylon/pull/188)) (Moti Zilberman) + +Add .gitattributes and .editorconfig for LF line endings ([#179](https://github.com/babel/babylon/pull/179)) (Moti Zilberman) + +Fixes two tests that are failing after the merge of #172 ([#177](https://github.com/babel/babylon/pull/177)) (Moti Zilberman) + +## v6.12.0 (2016-10-14) + +### :eyeglasses: Spec Compliance + +Implement import() syntax ([#163](https://github.com/babel/babylon/pull/163)) (Jordan Gensler) + +#### Dynamic Import + +- Proposal Repo: https://github.com/domenic/proposal-dynamic-import +- Championed by [@domenic](https://github.com/domenic) +- stage-2 +- [sept-28 tc39 notes](https://github.com/rwaldron/tc39-notes/blob/master/es7/2016-09/sept-28.md#113a-import) + +> This repository contains a proposal for adding a "function-like" import() module loading syntactic form to JavaScript + +```js +import(`./section-modules/${link.dataset.entryModule}.js`) +.then(module => { + module.loadPageInto(main); +}) +``` + +Add EmptyTypeAnnotation ([#171](https://github.com/babel/babylon/pull/171)) (Sam Goldman) + +#### EmptyTypeAnnotation + +Just wasn't covered before. + +```js +type T = empty; +``` + +### :bug: Bug Fix + +Fix crash when exporting with destructuring and sparse array ([#170](https://github.com/babel/babylon/pull/170)) (Jeroen Engels) + +```js +// was failing due to sparse array +export const { foo: [ ,, qux7 ] } = bar; +``` + +Allow keyword in Flow object declaration property names with type parameters ([#146](https://github.com/babel/babylon/pull/146)) (Dan Harper) + +```js +declare class X { + foobar(): void; + static foobar(): void; +} +``` + +Allow keyword in object/class property names with Flow type parameters ([#145](https://github.com/babel/babylon/pull/145)) (Dan Harper) + +```js +class Foo { + delete(item: T): T { + return item; + } +} +``` + +Allow typeAnnotations for yield expressions ([#174](https://github.com/babel/babylon/pull/174))) (Daniel Tschinder) + +```js +function *foo() { + const x = (yield 5: any); +} +``` + +### :nail_care: Polish + +Annotate more errors with expected token ([#172](https://github.com/babel/babylon/pull/172))) (Moti Zilberman) + +```js +// Unexpected token, expected ; (1:6) +{ set 1 } +``` + +### :house: Internal + +Remove kcheck ([#173](https://github.com/babel/babylon/pull/173))) (Daniel Tschinder) + +Also run flow, linting, babel tests on separate instances (add back node 0.10) + +## v6.11.6 (2016-10-12) + +### :bug: Bug Fix/Regression + +Fix crash when exporting with destructuring and sparse array ([#170](https://github.com/babel/babylon/pull/170)) (Jeroen Engels) + +```js +// was failing with `Cannot read property 'type' of null` because of null identifiers +export const { foo: [ ,, qux7 ] } = bar; +``` + +## v6.11.5 (2016-10-12) + +### :eyeglasses: Spec Compliance + +Fix: Check for duplicate named exports in exported destructuring assignments ([#144](https://github.com/babel/babylon/pull/144)) (Kai Cataldo) + +```js +// `foo` has already been exported. Exported identifiers must be unique. (2:20) +export function foo() {}; +export const { a: [{foo}] } = bar; +``` + +Fix: Check for duplicate named exports in exported rest elements/properties ([#164](https://github.com/babel/babylon/pull/164)) (Kai Cataldo) + +```js +// `foo` has already been exported. Exported identifiers must be unique. (2:22) +export const foo = 1; +export const [bar, ...foo] = baz; +``` + +### :bug: Bug Fix + +Fix: Allow identifier `async` for default param in arrow expression ([#165](https://github.com/babel/babylon/pull/165)) (Kai Cataldo) + +```js +// this is ok now +const test = ({async = true}) => {}; +``` + +### :nail_care: Polish + +Babylon will now print out the token it's expecting if there's a `SyntaxError` ([#150](https://github.com/babel/babylon/pull/150)) (Daniel Tschinder) + +```bash +# So in the case of a missing ending curly (`}`) +Module build failed: SyntaxError: Unexpected token, expected } (30:0) + 28 | } + 29 | +> 30 | + | ^ +``` + +## v6.11.4 (2016-10-03) + +Temporary rollback for erroring on trailing comma with spread (#154) (Henry Zhu) + +## v6.11.3 (2016-10-01) + +### :eyeglasses: Spec Compliance + +Add static errors for object rest (#149) ([@danez](https://github.com/danez)) + +> https://github.com/sebmarkbage/ecmascript-rest-spread + +Object rest copies the *rest* of properties from the right hand side `obj` starting from the left to right. + +```js +let { x, y, ...z } = { x: 1, y: 2, z: 3 }; +// x = 1 +// y = 2 +// z = { z: 3 } +``` + +#### New Syntax Errors: + +**SyntaxError**: The rest element has to be the last element when destructuring (1:10) +```bash +> 1 | let { ...x, y, z } = { x: 1, y: 2, z: 3}; + | ^ +# Previous behavior: +# x = { x: 1, y: 2, z: 3 } +# y = 2 +# z = 3 +``` + +Before, this was just a more verbose way of shallow copying `obj` since it doesn't actually do what you think. + +**SyntaxError**: Cannot have multiple rest elements when destructuring (1:13) + +```bash +> 1 | let { x, ...y, ...z } = { x: 1, y: 2, z: 3}; + | ^ +# Previous behavior: +# x = 1 +# y = { y: 2, z: 3 } +# z = { y: 2, z: 3 } +``` + +Before y and z would just be the same value anyway so there is no reason to need to have both. + +**SyntaxError**: A trailing comma is not permitted after the rest element (1:16) + +```js +let { x, y, ...z, } = obj; +``` + +The rationale for this is that the use case for trailing comma is that you can add something at the end without affecting the line above. Since a RestProperty always has to be the last property it doesn't make sense. + +--- + +get / set are valid property names in default assignment (#142) ([@jezell](https://github.com/jezell)) + +```js +// valid +function something({ set = null, get = null }) {} +``` + +## v6.11.2 (2016-09-23) + +### Bug Fix + +- [#139](https://github.com/babel/babylon/issues/139) Don't do the duplicate check if not an identifier (#140) @hzoo + +```js +// regression with duplicate export check +SyntaxError: ./typography.js: `undefined` has already been exported. Exported identifiers must be unique. (22:13) + 20 | + 21 | export const { rhythm } = typography; +> 22 | export const { TypographyStyle } = typography +``` + +Bail out for now, and make a change to account for destructuring in the next release. + +## 6.11.1 (2016-09-22) + +### Bug Fix +- [#137](https://github.com/babel/babylon/pull/137) - Fix a regression with duplicate exports - it was erroring on all keys in `Object.prototype`. @danez + +```javascript +export toString from './toString'; +``` + +```bash +`toString` has already been exported. Exported identifiers must be unique. (1:7) +> 1 | export toString from './toString'; + | ^ + 2 | +``` + +## 6.11.0 (2016-09-22) + +### Spec Compliance (will break CI) + +- Disallow duplicate named exports ([#107](https://github.com/babel/babylon/pull/107)) @kaicataldo + +```js +// Only one default export allowed per module. (2:9) +export default function() {}; +export { foo as default }; + +// Only one default export allowed per module. (2:0) +export default {}; +export default function() {}; + +// `Foo` has already been exported. Exported identifiers must be unique. (2:0) +export { Foo }; +export class Foo {}; +``` + +### New Feature (Syntax) + +- Add support for computed class property names ([#121](https://github.com/babel/babylon/pull/121)) @motiz88 + +```js +// AST +interface ClassProperty <: Node { + type: "ClassProperty"; + key: Identifier; + value: Expression; + computed: boolean; // added +} +``` + +```js +// with "plugins": ["classProperties"] +class Foo { + [x] + ['y'] +} + +class Bar { + [p] + [m] () {} +} + ``` + +### Bug Fix + +- Fix `static` property falling through in the declare class Flow AST ([#135](https://github.com/babel/babylon/pull/135)) @danharper + +```js +declare class X { + a: number; + static b: number; // static + c: number; // this was being marked as static in the AST as well +} +``` + +### Polish + +- Rephrase "assigning/binding to rvalue" errors to include context ([#119](https://github.com/babel/babylon/pull/119)) @motiz88 + +```js +// Used to error with: +// SyntaxError: Assigning to rvalue (1:0) + +// Now: +// Invalid left-hand side in assignment expression (1:0) +3 = 4 + +// Invalid left-hand side in for-in statement (1:5) +for (+i in {}); +``` + +### Internal + +- Fix call to `this.parseMaybeAssign` with correct arguments ([#133](https://github.com/babel/babylon/pull/133)) @danez +- Add semver note to changelog ([#131](https://github.com/babel/babylon/pull/131)) @hzoo + +## 6.10.0 (2016-09-19) + +> We plan to include some spec compliance bugs in patch versions. An example was the multiple default exports issue. + +### Spec Compliance + +* Implement ES2016 check for simple parameter list in strict mode ([#106](https://github.com/babel/babylon/pull/106)) (Timothy Gu) + +> It is a Syntax Error if ContainsUseStrict of FunctionBody is true and IsSimpleParameterList of FormalParameters is false. https://tc39.github.io/ecma262/2016/#sec-function-definitions-static-semantics-early-errors + +More Context: [tc39-notes](https://github.com/rwaldron/tc39-notes/blob/master/es7/2015-07/july-29.md#611-the-scope-of-use-strict-with-respect-to-destructuring-in-parameter-lists) + +For example: + +```js +// this errors because it uses destructuring and default parameters +// in a function with a "use strict" directive +function a([ option1, option2 ] = []) { + "use strict"; +} + ``` + +The solution would be to use a top level "use strict" or to remove the destructuring or default parameters when using a function + "use strict" or to. + +### New Feature + +* Exact object type annotations for Flow plugin ([#104](https://github.com/babel/babylon/pull/104)) (Basil Hosmer) + +Added to flow in https://github.com/facebook/flow/commit/c710c40aa2a115435098d6c0dfeaadb023cd39b8 + +Looks like: + +```js +var a : {| x: number, y: string |} = { x: 0, y: 'foo' }; +``` + +### Bug Fixes + +* Include `typeParameter` location in `ArrowFunctionExpression` ([#126](https://github.com/babel/babylon/pull/126)) (Daniel Tschinder) +* Error on invalid flow type annotation with default assignment ([#122](https://github.com/babel/babylon/pull/122)) (Dan Harper) +* Fix Flow return types on arrow functions ([#124](https://github.com/babel/babylon/pull/124)) (Dan Harper) + +### Misc + +* Add tests for export extensions ([#127](https://github.com/babel/babylon/pull/127)) (Daniel Tschinder) +* Fix Contributing guidelines [skip ci] (Daniel Tschinder) + +## 6.9.2 (2016-09-09) + +The only change is to remove the `babel-runtime` dependency by compiling with Babel's ES2015 loose mode. So using babylon standalone should be smaller. + +## 6.9.1 (2016-08-23) + +This release contains mainly small bugfixes but also updates babylons default mode to es2017. The features for `exponentiationOperator`, `asyncFunctions` and `trailingFunctionCommas` which previously needed to be activated via plugin are now enabled by default and the plugins are now no-ops. + +### Bug Fixes + +- Fix issues with default object params in async functions ([#96](https://github.com/babel/babylon/pull/96)) @danez +- Fix issues with flow-types and async function ([#95](https://github.com/babel/babylon/pull/95)) @danez +- Fix arrow functions with destructuring, types & default value ([#94](https://github.com/babel/babylon/pull/94)) @danharper +- Fix declare class with qualified type identifier ([#97](https://github.com/babel/babylon/pull/97)) @danez +- Remove exponentiationOperator, asyncFunctions, trailingFunctionCommas plugins and enable them by default ([#98](https://github.com/babel/babylon/pull/98)) @danez + +## 6.9.0 (2016-08-16) + +### New syntax support + +- Add JSX spread children ([#42](https://github.com/babel/babylon/pull/42)) @calebmer + +(Be aware that React is not going to support this syntax) + +```js +
+ {...todos.map(todo => )} +
+``` + +- Add support for declare module.exports ([#72](https://github.com/babel/babylon/pull/72)) @danez + +```js +declare module "foo" { + declare module.exports: {} +} +``` + +### New Features + +- If supplied, attach filename property to comment node loc. ([#80](https://github.com/babel/babylon/pull/80)) @divmain +- Add identifier name to node loc field ([#90](https://github.com/babel/babylon/pull/90)) @kittens + +### Bug Fixes + +- Fix exponential operator to behave according to spec ([#75](https://github.com/babel/babylon/pull/75)) @danez +- Fix lookahead to not add comments to arrays which are not cloned ([#76](https://github.com/babel/babylon/pull/76)) @danez +- Fix accidental fall-through in Flow type parsing. ([#82](https://github.com/babel/babylon/pull/82)) @xiemaisi +- Only allow declares inside declare module ([#73](https://github.com/babel/babylon/pull/73)) @danez +- Small fix for parsing type parameter declarations ([#83](https://github.com/babel/babylon/pull/83)) @gabelevi +- Fix arrow param locations with flow types ([#57](https://github.com/babel/babylon/pull/57)) @danez +- Fixes SyntaxError position with flow optional type ([#65](https://github.com/babel/babylon/pull/65)) @danez + +### Internal + +- Add codecoverage to tests @danez +- Fix tests to not save expected output if we expect the test to fail @danez +- Make a shallow clone of babel for testing @danez +- chore(package): update cross-env to version 2.0.0 ([#77](https://github.com/babel/babylon/pull/77)) @greenkeeperio-bot +- chore(package): update ava to version 0.16.0 ([#86](https://github.com/babel/babylon/pull/86)) @greenkeeperio-bot +- chore(package): update babel-plugin-istanbul to version 2.0.0 ([#89](https://github.com/babel/babylon/pull/89)) @greenkeeperio-bot +- chore(package): update nyc to version 8.0.0 ([#88](https://github.com/babel/babylon/pull/88)) @greenkeeperio-bot + +## 6.8.4 (2016-07-06) + +### Bug Fixes + +- Fix the location of params, when flow and default value used ([#68](https://github.com/babel/babylon/pull/68)) @danez + +## 6.8.3 (2016-07-02) + +### Bug Fixes + +- Fix performance regression introduced in 6.8.2 with conditionals ([#63](https://github.com/babel/babylon/pull/63)) @danez + +## 6.8.2 (2016-06-24) + +### Bug Fixes + +- Fix parse error with yielding jsx elements in generators `function* it() { yield ; }` ([#31](https://github.com/babel/babylon/pull/31)) @eldereal +- When cloning nodes do not clone its comments ([#24](https://github.com/babel/babylon/pull/24)) @danez +- Fix parse errors when using arrow functions with an spread element and return type `(...props): void => {}` ([#10](https://github.com/babel/babylon/pull/10)) @danez +- Fix leading comments added from previous node ([#23](https://github.com/babel/babylon/pull/23)) @danez +- Fix parse errors with flow's optional arguments `(arg?) => {}` ([#19](https://github.com/babel/babylon/pull/19)) @danez +- Support negative numeric type literals @kittens +- Remove line terminator restriction after await keyword @kittens +- Remove grouped type arrow restriction as it seems flow no longer has it @kittens +- Fix parse error with generic methods that have the name `get` or `set` `class foo { get() {} }` ([#55](https://github.com/babel/babylon/pull/55)) @vkurchatkin +- Fix parse error with arrow functions that have flow type parameter declarations `(x: T): T => x;` ([#54](https://github.com/babel/babylon/pull/54)) @gabelevi + +### Documentation + +- Document AST differences from ESTree ([#41](https://github.com/babel/babylon/pull/41)) @nene +- Move ast spec from babel/babel ([#46](https://github.com/babel/babylon/pull/46)) @hzoo + +### Internal + +- Enable skipped tests ([#16](https://github.com/babel/babylon/pull/16)) @danez +- Add script to test latest version of babylon with babel ([#21](https://github.com/babel/babylon/pull/21)) @danez +- Upgrade test runner ava @kittens +- Add missing generate-identifier-regex script @kittens +- Rename parser context types @kittens +- Add node v6 to travis testing @hzoo +- Update to Unicode v9 ([#45](https://github.com/babel/babylon/pull/45)) @mathiasbynens + +## 6.8.1 (2016-06-06) + +### New Feature + +- Parse type parameter declarations with defaults like `type Foo = T` + +### Bug Fixes +- Type parameter declarations need 1 or more type parameters. +- The existential type `*` is not a valid type parameter. +- The existential type `*` is a primary type + +### Spec Compliance +- The param list for type parameter declarations now consists of `TypeParameter` nodes +- New `TypeParameter` AST Node (replaces using the `Identifier` node before) + +``` +interface TypeParameter <: Node { + bound: TypeAnnotation; + default: TypeAnnotation; + name: string; + variance: "plus" | "minus"; +} +``` + +## 6.8.0 (2016-05-02) + +#### New Feature + +##### Parse Method Parameter Decorators ([#12](https://github.com/babel/babylon/pull/12)) + +> [Method Parameter Decorators](https://goo.gl/8MmCMG) is now a TC39 [stage 0 proposal](https://github.com/tc39/ecma262/blob/master/stage0.md). + +Examples: + +```js +class Foo { + constructor(@foo() x, @bar({ a: 123 }) @baz() y) {} +} + +export default function func(@foo() x, @bar({ a: 123 }) @baz() y) {} + +var obj = { + method(@foo() x, @bar({ a: 123 }) @baz() y) {} +}; +``` + +##### Parse for-await statements (w/ `asyncGenerators` plugin) ([#17](https://github.com/babel/babylon/pull/17)) + +There is also a new node type, `ForAwaitStatement`. + +> [Async generators and for-await](https://github.com/tc39/proposal-async-iteration) are now a [stage 2 proposal](https://github.com/tc39/ecma262#current-proposals). + +Example: + +```js +async function f() { + for await (let x of y); +} +``` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/LICENSE new file mode 100644 index 00000000..d4c7fc58 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2012-2014 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/README.md new file mode 100644 index 00000000..a9463e81 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/README.md @@ -0,0 +1,19 @@ +# @babel/parser + +> A JavaScript parser + +See our website [@babel/parser](https://babeljs.io/docs/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%22+is%3Aopen) associated with this package. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/parser +``` + +or using yarn: + +```sh +yarn add @babel/parser --dev +``` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/bin/babel-parser.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/bin/babel-parser.js new file mode 100755 index 00000000..4808c5ee --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/bin/babel-parser.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node +/* eslint-disable no-var, unicorn/prefer-node-protocol */ + +var parser = require(".."); +var fs = require("fs"); + +var filename = process.argv[2]; +if (!filename) { + console.error("no filename specified"); +} else { + var file = fs.readFileSync(filename, "utf8"); + var ast = parser.parse(file); + + console.log(JSON.stringify(ast, null, " ")); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/package.json new file mode 100644 index 00000000..817f2e49 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/package.json @@ -0,0 +1,50 @@ +{ + "name": "@babel/parser", + "version": "7.29.0", + "description": "A JavaScript parser", + "author": "The Babel Team (https://babel.dev/team)", + "homepage": "https://babel.dev/docs/en/next/babel-parser", + "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "babel", + "javascript", + "parser", + "tc39", + "ecmascript", + "@babel/parser" + ], + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-parser" + }, + "main": "./lib/index.js", + "types": "./typings/babel-parser.d.ts", + "files": [ + "bin", + "lib", + "typings/babel-parser.d.ts", + "index.cjs" + ], + "engines": { + "node": ">=6.0.0" + }, + "# dependencies": "This package doesn't actually have runtime dependencies. @babel/types is only needed for type definitions.", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "devDependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/helper-check-duplicate-nodes": "^7.28.6", + "@babel/helper-fixtures": "^7.28.6", + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "charcodes": "^0.2.0" + }, + "bin": "./bin/babel-parser.js", + "type": "commonjs" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/typings/babel-parser.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/typings/babel-parser.d.ts new file mode 100644 index 00000000..d083b0ab --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/typings/babel-parser.d.ts @@ -0,0 +1,262 @@ +// This file is auto-generated! Do not modify it directly. +// Run `yarn gulp bundle-dts` to re-generate it. +/* eslint-disable @typescript-eslint/consistent-type-imports, @typescript-eslint/no-redundant-type-constituents */ +import { File, Expression } from '@babel/types'; + +declare class Position { + line: number; + column: number; + index: number; + constructor(line: number, col: number, index: number); +} + +type SyntaxPlugin = "flow" | "typescript" | "jsx" | "pipelineOperator" | "placeholders"; +type ParseErrorCode = "BABEL_PARSER_SYNTAX_ERROR" | "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED"; +interface ParseErrorSpecification { + code: ParseErrorCode; + reasonCode: string; + syntaxPlugin?: SyntaxPlugin; + missingPlugin?: string | string[]; + loc: Position; + details: ErrorDetails; + pos: number; +} +type ParseError$1 = SyntaxError & ParseErrorSpecification; + +type BABEL_8_BREAKING = false; +type IF_BABEL_7 = false extends BABEL_8_BREAKING ? V : never; + +type Plugin$1 = + | "asyncDoExpressions" + | IF_BABEL_7<"asyncGenerators"> + | IF_BABEL_7<"bigInt"> + | IF_BABEL_7<"classPrivateMethods"> + | IF_BABEL_7<"classPrivateProperties"> + | IF_BABEL_7<"classProperties"> + | IF_BABEL_7<"classStaticBlock"> + | IF_BABEL_7<"decimal"> + | "decorators-legacy" + | "deferredImportEvaluation" + | "decoratorAutoAccessors" + | "destructuringPrivate" + | IF_BABEL_7<"deprecatedImportAssert"> + | "doExpressions" + | IF_BABEL_7<"dynamicImport"> + | IF_BABEL_7<"explicitResourceManagement"> + | "exportDefaultFrom" + | IF_BABEL_7<"exportNamespaceFrom"> + | "flow" + | "flowComments" + | "functionBind" + | "functionSent" + | "importMeta" + | "jsx" + | IF_BABEL_7<"jsonStrings"> + | IF_BABEL_7<"logicalAssignment"> + | IF_BABEL_7<"importAssertions"> + | IF_BABEL_7<"importReflection"> + | "moduleBlocks" + | IF_BABEL_7<"moduleStringNames"> + | IF_BABEL_7<"nullishCoalescingOperator"> + | IF_BABEL_7<"numericSeparator"> + | IF_BABEL_7<"objectRestSpread"> + | IF_BABEL_7<"optionalCatchBinding"> + | IF_BABEL_7<"optionalChaining"> + | "partialApplication" + | "placeholders" + | IF_BABEL_7<"privateIn"> + | IF_BABEL_7<"regexpUnicodeSets"> + | "sourcePhaseImports" + | "throwExpressions" + | IF_BABEL_7<"topLevelAwait"> + | "v8intrinsic" + | ParserPluginWithOptions[0]; + +type ParserPluginWithOptions = + | ["decorators", DecoratorsPluginOptions] + | ["discardBinding", { syntaxType: "void" }] + | ["estree", { classFeatures?: boolean }] + | IF_BABEL_7<["importAttributes", { deprecatedAssertSyntax: boolean }]> + | IF_BABEL_7<["moduleAttributes", { version: "may-2020" }]> + | ["optionalChainingAssign", { version: "2023-07" }] + | ["pipelineOperator", PipelineOperatorPluginOptions] + | ["recordAndTuple", RecordAndTuplePluginOptions] + | ["flow", FlowPluginOptions] + | ["typescript", TypeScriptPluginOptions]; + +type PluginConfig = Plugin$1 | ParserPluginWithOptions; + +interface DecoratorsPluginOptions { + decoratorsBeforeExport?: boolean; + allowCallParenthesized?: boolean; +} + +interface PipelineOperatorPluginOptions { + proposal: BABEL_8_BREAKING extends false + ? "minimal" | "fsharp" | "hack" | "smart" + : "fsharp" | "hack"; + topicToken?: "%" | "#" | "@@" | "^^" | "^"; +} + +interface RecordAndTuplePluginOptions { + syntaxType: "bar" | "hash"; +} + +type FlowPluginOptions = BABEL_8_BREAKING extends true + ? { + all?: boolean; + enums?: boolean; + } + : { + all?: boolean; + }; + +interface TypeScriptPluginOptions { + dts?: boolean; + disallowAmbiguousJSXLike?: boolean; +} + +type Plugin = PluginConfig; + +type SourceType = "script" | "commonjs" | "module" | "unambiguous"; +interface Options { + /** + * By default, import and export declarations can only appear at a program's top level. + * Setting this option to true allows them anywhere where a statement is allowed. + */ + allowImportExportEverywhere?: boolean; + /** + * By default, await use is not allowed outside of an async function. + * Set this to true to accept such code. + */ + allowAwaitOutsideFunction?: boolean; + /** + * By default, a return statement at the top level raises an error. + * Set this to true to accept such code. + */ + allowReturnOutsideFunction?: boolean; + /** + * By default, new.target use is not allowed outside of a function or class. + * Set this to true to accept such code. + */ + allowNewTargetOutsideFunction?: boolean; + /** + * By default, super calls are not allowed outside of a method. + * Set this to true to accept such code. + */ + allowSuperOutsideMethod?: boolean; + /** + * By default, exported identifiers must refer to a declared variable. + * Set this to true to allow export statements to reference undeclared variables. + */ + allowUndeclaredExports?: boolean; + /** + * By default, yield use is not allowed outside of a generator function. + * Set this to true to accept such code. + */ + allowYieldOutsideFunction?: boolean; + /** + * By default, Babel parser JavaScript code according to Annex B syntax. + * Set this to `false` to disable such behavior. + */ + annexB?: boolean; + /** + * By default, Babel attaches comments to adjacent AST nodes. + * When this option is set to false, comments are not attached. + * It can provide up to 30% performance improvement when the input code has many comments. + * @babel/eslint-parser will set it for you. + * It is not recommended to use attachComment: false with Babel transform, + * as doing so removes all the comments in output code, and renders annotations such as + * /* istanbul ignore next *\/ nonfunctional. + */ + attachComment?: boolean; + /** + * By default, Babel always throws an error when it finds some invalid code. + * When this option is set to true, it will store the parsing error and + * try to continue parsing the invalid input file. + */ + errorRecovery?: boolean; + /** + * Indicate the mode the code should be parsed in. + * Can be one of "script", "commonjs", "module", or "unambiguous". Defaults to "script". + * "unambiguous" will make @babel/parser attempt to guess, based on the presence + * of ES6 import or export statements. + * Files with ES6 imports and exports are considered "module" and are otherwise "script". + * + * Use "commonjs" to parse code that is intended to be run in a CommonJS environment such as Node.js. + */ + sourceType?: SourceType; + /** + * Correlate output AST nodes with their source filename. + * Useful when generating code and source maps from the ASTs of multiple input files. + */ + sourceFilename?: string; + /** + * By default, all source indexes start from 0. + * You can provide a start index to alternatively start with. + * Useful for integration with other source tools. + */ + startIndex?: number; + /** + * By default, the first line of code parsed is treated as line 1. + * You can provide a line number to alternatively start with. + * Useful for integration with other source tools. + */ + startLine?: number; + /** + * By default, the parsed code is treated as if it starts from line 1, column 0. + * You can provide a column number to alternatively start with. + * Useful for integration with other source tools. + */ + startColumn?: number; + /** + * Array containing the plugins that you want to enable. + */ + plugins?: Plugin[]; + /** + * Should the parser work in strict mode. + * Defaults to true if sourceType === 'module'. Otherwise, false. + */ + strictMode?: boolean; + /** + * Adds a ranges property to each node: [node.start, node.end] + */ + ranges?: boolean; + /** + * Adds all parsed tokens to a tokens property on the File node. + */ + tokens?: boolean; + /** + * By default, the parser adds information about parentheses by setting + * `extra.parenthesized` to `true` as needed. + * When this option is `true` the parser creates `ParenthesizedExpression` + * AST nodes instead of using the `extra` property. + */ + createParenthesizedExpressions?: boolean; + /** + * The default is false in Babel 7 and true in Babel 8 + * Set this to true to parse it as an `ImportExpression` node. + * Otherwise `import(foo)` is parsed as `CallExpression(Import, [Identifier(foo)])`. + */ + createImportExpressions?: boolean; +} + +type ParserOptions = Partial; +type ParseError = ParseError$1; +type ParseResult = Result & { + comments: File["comments"]; + errors: null | ParseError[]; + tokens?: File["tokens"]; +}; +/** + * Parse the provided code as an entire ECMAScript program. + */ +declare function parse(input: string, options?: ParserOptions): ParseResult; +declare function parseExpression(input: string, options?: ParserOptions): ParseResult; + +declare const tokTypes: { + // todo(flow->ts) real token type + [name: string]: any; +}; + +export { DecoratorsPluginOptions, FlowPluginOptions, ParseError, ParseResult, ParserOptions, PluginConfig as ParserPlugin, ParserPluginWithOptions, PipelineOperatorPluginOptions, RecordAndTuplePluginOptions, TypeScriptPluginOptions, parse, parseExpression, tokTypes }; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/LICENSE new file mode 100644 index 00000000..f31575ec --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/README.md new file mode 100644 index 00000000..54c9f819 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/README.md @@ -0,0 +1,19 @@ +# @babel/types + +> Babel Types is a Lodash-esque utility library for AST nodes + +See our website [@babel/types](https://babeljs.io/docs/babel-types) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20types%22+is%3Aopen) associated with this package. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/types +``` + +or using yarn: + +```sh +yarn add @babel/types --dev +``` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/package.json new file mode 100644 index 00000000..db753538 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/package.json @@ -0,0 +1,39 @@ +{ + "name": "@babel/types", + "version": "7.29.0", + "description": "Babel Types is a Lodash-esque utility library for AST nodes", + "author": "The Babel Team (https://babel.dev/team)", + "homepage": "https://babel.dev/docs/en/next/babel-types", + "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20types%22+is%3Aopen", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-types" + }, + "main": "./lib/index.js", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "devDependencies": { + "@babel/generator": "^7.29.0", + "@babel/helper-fixtures": "^7.28.6", + "@babel/parser": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "type": "commonjs", + "types": "./lib/index-legacy.d.ts", + "typesVersions": { + ">=4.1": { + "lib/index-legacy.d.ts": [ + "lib/index.d.ts" + ] + } + } +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.editorconfig b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.editorconfig new file mode 100644 index 00000000..86a63dc0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.gitattributes b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.gitattributes new file mode 100644 index 00000000..4b2c1a29 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.gitattributes @@ -0,0 +1,2 @@ +# Enforce `lf` for text files (even on Windows) +text eol=lf diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/CHANGELOG.md new file mode 100644 index 00000000..7300dec3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/CHANGELOG.md @@ -0,0 +1,250 @@ +## Next + +- **[Breaking change]** Replace `OutModules` enum by custom compiler option `mjsModule`. +- **[Breaking change]** Drop support for Pug, Sass, Angular & Webpack. +- **[Feature]** Expose custom registries for each target. +- **[Feature]** Add `dist.tscOptions` for `lib` target to override options for + distribution builds. +- **[Feature]** Native ESM tests with mocha. +- **[Fix]** Disable deprecated TsLint rules from the default config +- **[Fix]** Remove use of experimental `fs/promises` module. +- **[Internal]** Fix continuous deployment script (stop confusing PRs to master + with push to master) +- **[Internal]** Update dependencies +- **[Internal]** Fix deprecated Mocha types. + +## 0.17.1 (2017-05-03) + +- **[Fix]** Update dependencies, remove `std/esm` warning. + +## 0.17.0 (2017-04-22) + +- **[Breaking change]** Update dependencies. Use `esm` instead of `@std/esm`, update Typescript to `2.8.3`. +- **[Fix]** Fix Node processes spawn on Windows (Mocha, Nyc) + +## 0.16.2 (2017-02-07) + +- **[Fix]** Fix Typedoc generation: use `tsconfig.json` generated for the lib. +- **[Fix]** Write source map for `.mjs` files +- **[Fix]** Copy sources to `_src` when publishing a lib (#87). +- **[Internal]** Restore continuous deployment of documentation. + +## 0.16.1 (2017-01-20) + +- **[Feature]** Support `mocha` tests on `.mjs` files (using `@std/esm`). Enabled by default + if `outModules` is configured to emit `.mjs`. **You currently need to add + `"@std/esm": {"esm": "cjs"}` to your `package.json`.** + +## 0.16.0 (2017-01-09) + +- **[Breaking change]** Enable `allowSyntheticDefaultImports` and `esModuleInterop` by default +- **[Fix]** Allow deep module imports in default Tslint rules +- **[Fix]** Drop dependency on deprecated `gulp-util` +- **[Internal]** Replace most custom typings by types from `@types` + +## 0.15.8 (2017-12-05) + +- **[Fix]** Exit with non-zero code if command tested with coverage fails +- **[Fix]** Solve duplicated error message when using the `run` mocha task. +- **[Fix]** Exit with non-zero code when building scripts fails. + +## 0.15.7 (2017-11-29) + +- **[Feature]** Add `coverage` task to `mocha` target, use it for the default task + +## 0.15.6 (2017-11-29) + +- **[Fix]** Fix path to source in source maps. +- **[Fix]** Disable `number-literal-format` in default Tslint rules. It enforced uppercase for hex. +- **[Internal]** Enable integration with Greenkeeper. +- **[Internal]** Enable integration with Codecov +- **[Internal]** Enable code coverage + +## 0.15.5 (2017-11-10) + +- **[Feature]** Enable the following TsLint rules: `no-duplicate-switch-case`, `no-implicit-dependencies`, + `no-return-await` +- **[Internal]** Update self-dependency `0.15.4`, this restores the README on _npm_ +- **[Internal]** Add homepage and author fields to package.json + +## 0.15.4 (2017-11-10) + +- **[Fix]** Add support for custom additional copy for distribution builds. [#49](https://github.com/demurgos/turbo-gulp/issues/49) +- **[Internal]** Update self-dependency to `turbo-gulp` +- **[Internal]** Add link to license in `README.md` + +## 0.15.3 (2017-11-09) + +**Rename to `turbo-gulp`**. This package was previously named `demurgos-web-build-tools`. +This version is fully compatible: you can just change the name of your dependency. + +## 0.15.2 (2017-11-09) + +**The package is prepared to be renamed `turbo-gulp`.** +This is the last version released as `demurgos-web-build-tools`. + +- **[Feature]** Add support for watch mode for library targets. +- **[Fix]** Disable experimental support for `*.mjs` by default. +- **[Fix]** Do not emit duplicate TS errors + +## 0.15.1 (2017-10-19) + +- **[Feature]** Add experimental support for `*.mjs` files +- **[Fix]** Fix support of releases from Continuous Deployment using Travis. + +## 0.15.0 (2017-10-18) + +- **[Fix]** Add error handling for git deployment. +- **[Internal]** Enable continuous deployment of the `master` branch. + +## 0.15.0-beta.11 (2017-08-29) + +- **[Feature]** Add `LibTarget.dist.copySrc` option to disable copy of source files to the dist directory. + This allows to prevent issues with missing custom typings. +- **[Fix]** Mark `deploy` property of `LibTarget.typedoc` as optional. +- **[Internal]** Update self-dependency to `v0.15.0-beta.10`. + +## 0.15.0-beta.10 (2017-08-28) + +- **[Breaking]** Update Tslint rules to use `tslint@5.7.0`. +- **[Fix]** Set `allowJs` to false in default TSC options. +- **[Fix]** Do not pipe output of git commands to stdout. +- **[Internal]** Update self-dependency to `v0.15.0-beta.9`. + +## 0.15.0-beta.9 (2017-08-28) + +- **[Breaking]** Drop old-style `test` target. +- **[Breaking]** Drop old-style `node` target. +- **[Feature]** Add `mocha` target to run tests in `spec.ts` files. +- **[Feature]** Add `node` target to build and run top-level Node applications. +- **[Feature]** Provide `generateNodeTasks`, `generateLibTasks` and `generateMochaTasks` functions. + They create the tasks but do not register them. +- **[Fix]** Run `clean` before `dist`, if defined. +- **[Fix]** Run `dist` before `publish`. + +## 0.15.0-beta.8 (2017-08-26) + +- **[Fix]** Remove auth token and registry options for `:dist:publish`. It is better served + by configuring the environment appropriately. + +## 0.15.0-beta.7 (2017-08-26) + +- **[Feature]** Add `clean` task to `lib` targets. +- **[Fix]** Ensure that `gitHead` is defined when publishing a package to npm. + +## 0.15.0-beta.6 (2017-08-22) + +- **[Feature]** Add support for Typedoc deployment to a remote git branch (such as `gh-pages`) +- **[Feature]** Add support for `copy` tasks in new library target. +- **[Fix]** Resolve absolute paths when compiling scripts with custom typings. + +## 0.15.0-beta.5 (2017-08-14) + +- **[Fix]** Fix package entry for the main module. + +## 0.15.0-beta.4 (2017-08-14) + +- **[Breaking]** Drop ES5 build exposed to browsers with the `browser` field in `package.json`. +- **[Feature]** Introduce first new-style target (`LibTarget`). it supports typedoc generation, dev builds and + simple distribution. + +## 0.15.0-beta.3 (2017-08-11) + +- **[Breaking]** Update default lib target to use target-specific `srcDir`. +- **[Feature]** Allow to complete `srcDir` in target. +- **[Feature]** Add experimental library distribution supporting deep requires. + +## 0.15.0-beta.2 (2017-08-10) + +- **[Fix]** Default to CommonJS for project tsconfig.json +- **[Fix]** Add Typescript configuration for default project. +- **[Internal]** Update self-dependency to `0.15.0-beta.1`. + +## 0.15.0-beta.1 (2017-08-09) + +- **[Feature]** Support typed TSLint rules. +- **[Internal]** Update gulpfile.ts to use build tools `0.15.0-beta.0`. +- **[Fix]** Fix regressions caused by `0.15.0-beta.0` (missing type definition). + +## 0.15.0-beta.0 (2017-08-09) + +- **[Breaking]** Expose option interfaces directly in the main module instead of the `config` namespace. +- **[Breaking]** Rename `DEFAULT_PROJECT_OPTIONS` to `DEFAULT_PROJECT`. +- **[Feature]** Emit project-wide `tsconfig.json`. +- **[Internal]** Convert gulpfile to Typescript, use `ts-node` to run it. +- **[Internal]** Update dependencies + +## 0.14.3 (2017-07-16) + +- **[Feature]** Add `:lint:fix` project task to fix some lint errors. + +## 0.14.2 (2017-07-10) + +- **[Internal]** Update dependencies: add `package-lock.json` and update `tslint`. + +## 0.14.1 (2017-06-17) + +- **[Internal]** Update dependencies. +- **[Internal]** Drop dependency on _Bluebird_. +- **[Internal]** Drop dependency on _typings_. + +## 0.14.0 (2017-05-10) + +- **[Breaking]** Enforce trailing commas by default for multiline objects +- **[Feature]** Allow bump from either `master` or a branch with the same name as the tag (exampel: `v1.2.3`) +- **[Feature]** Support TSLint 8, allow to extend the default rules +- **[Patch]** Allow mergeable namespaces + +# 0.13.1 + +- **[Patch]** Allow namespaces in the default TS-Lint config + +# 0.13.0 + +- **[Breaking]** Major overhaul of the angular target. The server build no longer depends on the client. +- **[Breaking]** Update to `gulp@4` (from `gulp@3`) +- **[Breaking]** Update to `tslint@7` (from `tslint@6`), add stricter default rules +- **[Breaking]** Update signature of targetGenerators and project tasks: it only uses + `ProjectOptions` and `Target` now, the additional options are embedded in those two objects. +- **[Breaking]** Remove `:install`, `:instal:npm` and `:install:typings`. Use the `prepare` script in + your `package.json` file instead. +- Add `:tslint.json` project task to generate configuration for `tslint` +- Add first class support for processing of `pug` and `sass` files, similar to `copy` +- Implement end-to-end tests +- Enable `emitDecoratorMetadata` in default typescript options. +- Allow configuration of `:lint` with the `tslintOptions` property of the project configuration. +- Add `:watch` tasks for incremental builds. + +# 0.12.3 + +- Support `templateUrl` and `styleUrls` in angular modules. + +# 0.12.2 + +- Add `:build:copy` task. It copies user-defined files. + +# 0.12.1 + +- Fix `:watch` task. + +# 0.12.0 + +- **[Breaking]**: Change naming convention for tasks. The names primary part is + the target, then the action (`lib:build` instead of `build:lib`) to group + the tasks per target. +- **[Breaking]**: Use `typeRoots` instead of `definitions` in configuration to + specify Typescript definition files. +- Generate `tsconfig.json` file (mainly for editors) +- Implement the `test` target to run unit-tests with `mocha`. + +# 0.11.2 + +- Target `angular`: Add `build::assets:sass` for `.scss` files (Sassy CSS) + +# 0.11.1 + +- Rename project to `web-build-tools` (`demurgos-web-build-tools` on _npm_) +- Target `angular`: Add `build::assets`, `build::pug` and `build::static`. +- Update `gulp-typescript`: solve error message during compilation +- Targets `node` and `angular`: `build::scripts` now include in-lined source maps +- Target `node`: `watch:` to support incremental builds diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.md new file mode 100644 index 00000000..d588b5c3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2015-2017 Charles Samborski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.txt new file mode 100644 index 00000000..629264e9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.txt @@ -0,0 +1,14 @@ +Copyright (c) 2017, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/README.md new file mode 100644 index 00000000..eea761b9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/README.md @@ -0,0 +1,11 @@ +# V8 Coverage + +[![npm](https://img.shields.io/npm/v/@c88/v8-coverage.svg?maxAge=2592000)](https://www.npmjs.com/package/@c88/v8-coverage) +[![GitHub repository](https://img.shields.io/badge/Github-demurgos%2Fv8--coverage-blue.svg)](https://github.com/demurgos/v8-coverage) +[![Build status (Travis)](https://img.shields.io/travis/demurgos/v8-coverage/master.svg?maxAge=2592000)](https://travis-ci.org/demurgos/v8-coverage) +[![Build status (AppVeyor)](https://ci.appveyor.com/api/projects/status/qgcbdffyb9e09d0e?svg=true)](https://ci.appveyor.com/project/demurgos/v8-coverage) +[![Codecov](https://codecov.io/gh/demurgos/v8-coverage/branch/master/graph/badge.svg)](https://codecov.io/gh/demurgos/v8-coverage) + +## License + +[MIT License](./LICENSE.md) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/gulpfile.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/gulpfile.ts new file mode 100644 index 00000000..cdcfc818 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/gulpfile.ts @@ -0,0 +1,95 @@ +import * as buildTools from "turbo-gulp"; +import { LibTarget, registerLibTasks } from "turbo-gulp/targets/lib"; +import { MochaTarget, registerMochaTasks } from "turbo-gulp/targets/mocha"; + +import gulp from "gulp"; +import minimist from "minimist"; + +interface Options { + devDist?: string; +} + +const options: Options & minimist.ParsedArgs = minimist(process.argv.slice(2), { + string: ["devDist"], + default: {devDist: undefined}, + alias: {devDist: "dev-dist"}, +}); + +const project: buildTools.Project = { + root: __dirname, + packageJson: "package.json", + buildDir: "build", + distDir: "dist", + srcDir: "src", + typescript: {} +}; + +const lib: LibTarget = { + project, + name: "lib", + srcDir: "src/lib", + scripts: ["**/*.ts"], + mainModule: "index", + dist: { + packageJsonMap: (old: buildTools.PackageJson): buildTools.PackageJson => { + const version: string = options.devDist !== undefined ? `${old.version}-build.${options.devDist}` : old.version; + return {...old, version, scripts: undefined, private: false}; + }, + npmPublish: { + tag: options.devDist !== undefined ? "next" : "latest", + }, + }, + tscOptions: { + declaration: true, + skipLibCheck: true, + }, + typedoc: { + dir: "typedoc", + name: "Helpers for V8 coverage files", + deploy: { + repository: "git@github.com:demurgos/v8-coverage.git", + branch: "gh-pages", + }, + }, + copy: [ + { + files: ["**/*.json"], + }, + ], + clean: { + dirs: ["build/lib", "dist/lib"], + }, +}; + +const test: MochaTarget = { + project, + name: "test", + srcDir: "src", + scripts: ["test/**/*.ts", "lib/**/*.ts", "e2e/*/*.ts"], + customTypingsDir: "src/custom-typings", + tscOptions: { + allowSyntheticDefaultImports: true, + esModuleInterop: true, + skipLibCheck: true, + }, + // generateTestMain: true, + copy: [ + { + src: "e2e", + // /(project|test-resources)/ + files: ["*/project/**/*", "*/test-resources/**/*"], + dest: "e2e", + }, + ], + clean: { + dirs: ["build/test"], + }, +}; + +const libTasks: any = registerLibTasks(gulp, lib); +registerMochaTasks(gulp, test); +buildTools.projectTasks.registerAll(gulp, project); + +gulp.task("all:tsconfig.json", gulp.parallel("lib:tsconfig.json", "test:tsconfig.json")); +gulp.task("dist", libTasks.dist); +gulp.task("default", libTasks.dist); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/package.json new file mode 100644 index 00000000..abc28b78 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/package.json @@ -0,0 +1,48 @@ +{ + "name": "@bcoe/v8-coverage", + "version": "0.2.3", + "description": "Helper functions for V8 coverage files.", + "author": "Charles Samborski (https://demurgos.net)", + "license": "MIT", + "main": "dist/lib/index", + "types": "dist/lib/index.d.ts", + "repository": { + "type": "git", + "url": "git://github.com/demurgos/v8-coverage.git" + }, + "homepage": "https://demurgos.github.io/v8-coverage", + "scripts": { + "prepare": "gulp all:tsconfig.json && gulp dist", + "pretest": "gulp lib:build", + "test": "gulp test", + "lint": "gulp :lint:fix" + }, + "devDependencies": { + "@types/chai": "^4.1.4", + "@types/gulp": "^4.0.5", + "@types/minimist": "^1.2.0", + "@types/mocha": "^5.2.2", + "@types/node": "^10.5.4", + "chai": "^4.1.2", + "codecov": "^3.0.2", + "gulp": "^4.0.0", + "gulp-cli": "^2.0.1", + "minimist": "^1.2.0", + "pre-commit": "^1.2.2", + "ts-node": "^8.3.0", + "turbo-gulp": "^0.20.1" + }, + "nyc": { + "include": [ + "build/test/lib/**/*.js", + "build/test/lib/**/*.mjs" + ], + "reporter": [ + "text", + "html" + ], + "extension": [ + ".mjs" + ] + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/src/test/merge.spec.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/src/test/merge.spec.ts new file mode 100644 index 00000000..9d5522a2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/src/test/merge.spec.ts @@ -0,0 +1,280 @@ +import chai from "chai"; +import fs from "fs"; +import path from "path"; +import { FunctionCov, mergeFunctionCovs, mergeProcessCovs, mergeScriptCovs, ProcessCov, ScriptCov } from "../lib"; + +const REPO_ROOT: string = path.join(__dirname, "..", "..", "..", ".."); +const BENCHES_INPUT_DIR: string = path.join(REPO_ROOT, "benches"); +const BENCHES_DIR: string = path.join(REPO_ROOT, "test-data", "merge", "benches"); +const RANGES_DIR: string = path.join(REPO_ROOT, "test-data", "merge", "ranges"); +const BENCHES_TIMEOUT: number = 20000; // 20sec + +interface MergeRangeItem { + name: string; + status: "run" | "skip" | "only"; + inputs: ProcessCov[]; + expected: ProcessCov; +} + +const FIXTURES_DIR: string = path.join(REPO_ROOT, "test-data", "bugs"); +function loadFixture(name: string) { + const content: string = fs.readFileSync( + path.resolve(FIXTURES_DIR, `${name}.json`), + {encoding: "UTF-8"}, + ); + return JSON.parse(content); +} + +describe("merge", () => { + describe("Various", () => { + it("accepts empty arrays for `mergeProcessCovs`", () => { + const inputs: ProcessCov[] = []; + const expected: ProcessCov = {result: []}; + const actual: ProcessCov = mergeProcessCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + + it("accepts empty arrays for `mergeScriptCovs`", () => { + const inputs: ScriptCov[] = []; + const expected: ScriptCov | undefined = undefined; + const actual: ScriptCov | undefined = mergeScriptCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + + it("accepts empty arrays for `mergeFunctionCovs`", () => { + const inputs: FunctionCov[] = []; + const expected: FunctionCov | undefined = undefined; + const actual: FunctionCov | undefined = mergeFunctionCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + + it("accepts arrays with a single item for `mergeProcessCovs`", () => { + const inputs: ProcessCov[] = [ + { + result: [ + { + scriptId: "123", + url: "/lib.js", + functions: [ + { + functionName: "test", + isBlockCoverage: true, + ranges: [ + {startOffset: 0, endOffset: 4, count: 2}, + {startOffset: 1, endOffset: 2, count: 1}, + {startOffset: 2, endOffset: 3, count: 1}, + ], + }, + ], + }, + ], + }, + ]; + const expected: ProcessCov = { + result: [ + { + scriptId: "0", + url: "/lib.js", + functions: [ + { + functionName: "test", + isBlockCoverage: true, + ranges: [ + {startOffset: 0, endOffset: 4, count: 2}, + {startOffset: 1, endOffset: 3, count: 1}, + ], + }, + ], + }, + ], + }; + const actual: ProcessCov = mergeProcessCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + + describe("mergeProcessCovs", () => { + // see: https://github.com/demurgos/v8-coverage/issues/2 + it("handles function coverage merged into block coverage", () => { + const blockCoverage: ProcessCov = loadFixture("issue-2-block-coverage"); + const functionCoverage: ProcessCov = loadFixture("issue-2-func-coverage"); + const inputs: ProcessCov[] = [ + functionCoverage, + blockCoverage, + ]; + const expected: ProcessCov = loadFixture("issue-2-expected"); + const actual: ProcessCov = mergeProcessCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + + // see: https://github.com/demurgos/v8-coverage/issues/2 + it("handles block coverage merged into function coverage", () => { + const blockCoverage: ProcessCov = loadFixture("issue-2-block-coverage"); + const functionCoverage: ProcessCov = loadFixture("issue-2-func-coverage"); + const inputs: ProcessCov[] = [ + blockCoverage, + functionCoverage, + ]; + const expected: ProcessCov = loadFixture("issue-2-expected"); + const actual: ProcessCov = mergeProcessCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + }); + + it("accepts arrays with a single item for `mergeScriptCovs`", () => { + const inputs: ScriptCov[] = [ + { + scriptId: "123", + url: "/lib.js", + functions: [ + { + functionName: "test", + isBlockCoverage: true, + ranges: [ + {startOffset: 0, endOffset: 4, count: 2}, + {startOffset: 1, endOffset: 2, count: 1}, + {startOffset: 2, endOffset: 3, count: 1}, + ], + }, + ], + }, + ]; + const expected: ScriptCov | undefined = { + scriptId: "123", + url: "/lib.js", + functions: [ + { + functionName: "test", + isBlockCoverage: true, + ranges: [ + {startOffset: 0, endOffset: 4, count: 2}, + {startOffset: 1, endOffset: 3, count: 1}, + ], + }, + ], + }; + const actual: ScriptCov | undefined = mergeScriptCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + + it("accepts arrays with a single item for `mergeFunctionCovs`", () => { + const inputs: FunctionCov[] = [ + { + functionName: "test", + isBlockCoverage: true, + ranges: [ + {startOffset: 0, endOffset: 4, count: 2}, + {startOffset: 1, endOffset: 2, count: 1}, + {startOffset: 2, endOffset: 3, count: 1}, + ], + }, + ]; + const expected: FunctionCov = { + functionName: "test", + isBlockCoverage: true, + ranges: [ + {startOffset: 0, endOffset: 4, count: 2}, + {startOffset: 1, endOffset: 3, count: 1}, + ], + }; + const actual: FunctionCov | undefined = mergeFunctionCovs(inputs); + chai.assert.deepEqual(actual, expected); + }); + }); + + describe("ranges", () => { + for (const sourceFile of getSourceFiles()) { + const relPath: string = path.relative(RANGES_DIR, sourceFile); + describe(relPath, () => { + const content: string = fs.readFileSync(sourceFile, {encoding: "UTF-8"}); + const items: MergeRangeItem[] = JSON.parse(content); + for (const item of items) { + const test: () => void = () => { + const actual: ProcessCov | undefined = mergeProcessCovs(item.inputs); + chai.assert.deepEqual(actual, item.expected); + }; + switch (item.status) { + case "run": + it(item.name, test); + break; + case "only": + it.only(item.name, test); + break; + case "skip": + it.skip(item.name, test); + break; + default: + throw new Error(`Unexpected status: ${item.status}`); + } + } + }); + } + }); + + describe("benches", () => { + for (const bench of getBenches()) { + const BENCHES_TO_SKIP: Set = new Set(); + if (process.env.CI === "true") { + // Skip very large benchmarks when running continuous integration + BENCHES_TO_SKIP.add("node@10.11.0"); + BENCHES_TO_SKIP.add("npm@6.4.1"); + } + + const name: string = path.basename(bench); + + if (BENCHES_TO_SKIP.has(name)) { + it.skip(`${name} (skipped: too large for CI)`, testBench); + } else { + it(name, testBench); + } + + async function testBench(this: Mocha.Context) { + this.timeout(BENCHES_TIMEOUT); + + const inputFileNames: string[] = await fs.promises.readdir(bench); + const inputPromises: Promise[] = []; + for (const inputFileName of inputFileNames) { + const resolved: string = path.join(bench, inputFileName); + inputPromises.push(fs.promises.readFile(resolved).then(buffer => JSON.parse(buffer.toString("UTF-8")))); + } + const inputs: ProcessCov[] = await Promise.all(inputPromises); + const expectedPath: string = path.join(BENCHES_DIR, `${name}.json`); + const expectedContent: string = await fs.promises.readFile(expectedPath, {encoding: "UTF-8"}) as string; + const expected: ProcessCov = JSON.parse(expectedContent); + const startTime: number = Date.now(); + const actual: ProcessCov | undefined = mergeProcessCovs(inputs); + const endTime: number = Date.now(); + console.error(`Time (${name}): ${(endTime - startTime) / 1000}`); + chai.assert.deepEqual(actual, expected); + console.error(`OK: ${name}`); + } + } + }); +}); + +function getSourceFiles() { + return getSourcesFrom(RANGES_DIR); + + function* getSourcesFrom(dir: string): Iterable { + const names: string[] = fs.readdirSync(dir); + for (const name of names) { + const resolved: string = path.join(dir, name); + const stat: fs.Stats = fs.statSync(resolved); + if (stat.isDirectory()) { + yield* getSourcesFrom(dir); + } else { + yield resolved; + } + } + } +} + +function* getBenches(): Iterable { + const names: string[] = fs.readdirSync(BENCHES_INPUT_DIR); + for (const name of names) { + const resolved: string = path.join(BENCHES_INPUT_DIR, name); + const stat: fs.Stats = fs.statSync(resolved); + if (stat.isDirectory()) { + yield resolved; + } + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/tsconfig.json new file mode 100644 index 00000000..73db48fe --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/tsconfig.json @@ -0,0 +1,59 @@ +{ + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "charset": "utf8", + "checkJs": false, + "declaration": false, + "disableSizeLimit": false, + "downlevelIteration": false, + "emitBOM": false, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": false, + "inlineSourceMap": false, + "inlineSources": false, + "isolatedModules": false, + "lib": [ + "es2017", + "esnext.asynciterable" + ], + "locale": "en-us", + "module": "commonjs", + "moduleResolution": "node", + "newLine": "lf", + "noEmit": false, + "noEmitHelpers": false, + "noEmitOnError": true, + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noStrictGenericChecks": false, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitUseStrict": false, + "noLib": false, + "noResolve": false, + "preserveConstEnums": false, + "removeComments": false, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "suppressExcessPropertyErrors": false, + "suppressImplicitAnyIndexErrors": false, + "target": "es2017", + "traceResolution": false, + "typeRoots": [ + "src/lib/custom-typings", + "node_modules/@types" + ] + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/README.md new file mode 100644 index 00000000..b2f19300 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/README.md @@ -0,0 +1,3 @@ +# esbuild + +This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/bin/esbuild b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/bin/esbuild new file mode 100755 index 00000000..288f7689 Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/bin/esbuild differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/package.json new file mode 100644 index 00000000..b70b09ec --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/package.json @@ -0,0 +1,20 @@ +{ + "name": "@esbuild/linux-x64", + "version": "0.21.5", + "description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.", + "repository": { + "type": "git", + "url": "git+https://github.com/evanw/esbuild.git" + }, + "license": "MIT", + "preferUnplugged": true, + "engines": { + "node": ">=12" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ] +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/LICENSE new file mode 100644 index 00000000..5f84afaf --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/README.md new file mode 100644 index 00000000..af0e9311 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/README.md @@ -0,0 +1,358 @@ +# Node.js Adapter for Hono + +This adapter `@hono/node-server` allows you to run your Hono application on Node.js. +Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js. +It utilizes web standard APIs implemented in Node.js version 18 or higher. + +## Benchmarks + +Hono is 3.5 times faster than Express. + +Express: + +```txt +$ bombardier -d 10s --fasthttp http://localhost:3000/ + +Statistics Avg Stdev Max + Reqs/sec 16438.94 1603.39 19155.47 + Latency 7.60ms 7.51ms 559.89ms + HTTP codes: + 1xx - 0, 2xx - 164494, 3xx - 0, 4xx - 0, 5xx - 0 + others - 0 + Throughput: 4.55MB/s +``` + +Hono + `@hono/node-server`: + +```txt +$ bombardier -d 10s --fasthttp http://localhost:3000/ + +Statistics Avg Stdev Max + Reqs/sec 58296.56 5512.74 74403.56 + Latency 2.14ms 1.46ms 190.92ms + HTTP codes: + 1xx - 0, 2xx - 583059, 3xx - 0, 4xx - 0, 5xx - 0 + others - 0 + Throughput: 12.56MB/s +``` + +## Requirements + +It works on Node.js versions greater than 18.x. The specific required Node.js versions are as follows: + +- 18.x => 18.14.1+ +- 19.x => 19.7.0+ +- 20.x => 20.0.0+ + +Essentially, you can simply use the latest version of each major release. + +## Installation + +You can install it from the npm registry with `npm` command: + +```sh +npm install @hono/node-server +``` + +Or use `yarn`: + +```sh +yarn add @hono/node-server +``` + +## Usage + +Just import `@hono/node-server` at the top and write the code as usual. +The same code that runs on Cloudflare Workers, Deno, and Bun will work. + +```ts +import { serve } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono() +app.get('/', (c) => c.text('Hono meets Node.js')) + +serve(app, (info) => { + console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000 +}) +``` + +For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`. + +```sh +ts-node ./index.ts +``` + +Open `http://localhost:3000` with your browser. + +## Options + +### `port` + +```ts +serve({ + fetch: app.fetch, + port: 8787, // Port number, default is 3000 +}) +``` + +### `createServer` + +```ts +import { createServer } from 'node:https' +import fs from 'node:fs' + +//... + +serve({ + fetch: app.fetch, + createServer: createServer, + serverOptions: { + key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'), + cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'), + }, +}) +``` + +### `overrideGlobalObjects` + +The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`. + +```ts +serve({ + fetch: app.fetch, + overrideGlobalObjects: false, +}) +``` + +### `autoCleanupIncoming` + +The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`. + +If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`. + +```ts +serve({ + fetch: app.fetch, + autoCleanupIncoming: false, +}) +``` + +## Middleware + +Most built-in middleware also works with Node.js. +Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking. + +```ts +import { serve } from '@hono/node-server' +import { Hono } from 'hono' +import { prettyJSON } from 'hono/pretty-json' + +const app = new Hono() + +app.get('*', prettyJSON()) +app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' })) + +serve(app) +``` + +## Serve Static Middleware + +Use Serve Static Middleware that has been created for Node.js. + +```ts +import { serveStatic } from '@hono/node-server/serve-static' + +//... + +app.use('/static/*', serveStatic({ root: './' })) +``` + +If using a relative path, `root` will be relative to the current working directory from which the app was started. + +This can cause confusion when running your application locally. + +Imagine your project structure is: + +``` +my-hono-project/ + src/ + index.ts + static/ + index.html +``` + +Typically, you would run your app from the project's root directory (`my-hono-project`), +so you would need the following code to serve the `static` folder: + +```ts +app.use('/static/*', serveStatic({ root: './static' })) +``` + +Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`. + +### Options + +#### `rewriteRequestPath` + +If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following. + +```ts +app.use( + '/__foo/*', + serveStatic({ + root: './.foojs/', + rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''), + }) +) +``` + +#### `onFound` + +You can specify handling when the requested file is found with `onFound`. + +```ts +app.use( + '/static/*', + serveStatic({ + // ... + onFound: (_path, c) => { + c.header('Cache-Control', `public, immutable, max-age=31536000`) + }, + }) +) +``` + +#### `onNotFound` + +The `onNotFound` is useful for debugging. You can write a handle for when a file is not found. + +```ts +app.use( + '/static/*', + serveStatic({ + root: './non-existent-dir', + onNotFound: (path, c) => { + console.log(`${path} is not found, request to ${c.req.path}`) + }, + }) +) +``` + +#### `precompressed` + +The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file. + +```ts +app.use( + '/static/*', + serveStatic({ + precompressed: true, + }) +) +``` + +## ConnInfo Helper + +You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`. + +```ts +import { getConnInfo } from '@hono/node-server/conninfo' + +app.get('/', (c) => { + const info = getConnInfo(c) // info is `ConnInfo` + return c.text(`Your remote address is ${info.remote.address}`) +}) +``` + +## Accessing Node.js API + +You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following. + +```ts +import { serve } from '@hono/node-server' +import type { HttpBindings } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono<{ Bindings: HttpBindings }>() + +app.get('/', (c) => { + return c.json({ + remoteAddress: c.env.incoming.socket.remoteAddress, + }) +}) + +serve(app) +``` + +The APIs that you can get from `c.env` are as follows. + +```ts +type HttpBindings = { + incoming: IncomingMessage + outgoing: ServerResponse +} + +type Http2Bindings = { + incoming: Http2ServerRequest + outgoing: Http2ServerResponse +} +``` + +## Direct response from Node.js API + +You can directly respond to the client from the Node.js API. +In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`. + +> [!NOTE] +> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications. + +```ts +import { serve } from '@hono/node-server' +import type { HttpBindings } from '@hono/node-server' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import { Hono } from 'hono' + +const app = new Hono<{ Bindings: HttpBindings }>() + +app.get('/', (c) => { + const { outgoing } = c.env + outgoing.writeHead(200, { 'Content-Type': 'text/plain' }) + outgoing.end('Hello World\n') + + return RESPONSE_ALREADY_SENT +}) + +serve(app) +``` + +## Listen to a UNIX domain socket + +You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port. + +```ts +import { createAdaptorServer } from '@hono/node-server' + +// ... + +const socketPath = '/tmp/example.sock' + +const server = createAdaptorServer(app) +server.listen(socketPath, () => { + console.log(`Listening on ${socketPath}`) +}) +``` + +## Related projects + +- Hono - +- Hono GitHub repository - + +## Authors + +- Yusuke Wada +- Taku Amano + +## License + +MIT diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/package.json new file mode 100644 index 00000000..92c45085 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/package.json @@ -0,0 +1,103 @@ +{ + "name": "@hono/node-server", + "version": "1.19.11", + "description": "Node.js Adapter for Hono", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./serve-static": { + "types": "./dist/serve-static.d.ts", + "require": "./dist/serve-static.js", + "import": "./dist/serve-static.mjs" + }, + "./vercel": { + "types": "./dist/vercel.d.ts", + "require": "./dist/vercel.js", + "import": "./dist/vercel.mjs" + }, + "./utils/*": { + "types": "./dist/utils/*.d.ts", + "require": "./dist/utils/*.js", + "import": "./dist/utils/*.mjs" + }, + "./conninfo": { + "types": "./dist/conninfo.d.ts", + "require": "./dist/conninfo.js", + "import": "./dist/conninfo.mjs" + } + }, + "typesVersions": { + "*": { + ".": [ + "./dist/index.d.ts" + ], + "serve-static": [ + "./dist/serve-static.d.ts" + ], + "vercel": [ + "./dist/vercel.d.ts" + ], + "utils/*": [ + "./dist/utils/*.d.ts" + ], + "conninfo": [ + "./dist/conninfo.d.ts" + ] + } + }, + "scripts": { + "test": "node --expose-gc node_modules/jest/bin/jest.js", + "build": "tsup --external hono", + "watch": "tsup --watch", + "postbuild": "publint", + "prerelease": "bun run build && bun run test", + "release": "np", + "lint": "eslint src test", + "lint:fix": "eslint src test --fix", + "format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"", + "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/honojs/node-server.git" + }, + "homepage": "https://github.com/honojs/node-server", + "author": "Yusuke Wada (https://github.com/yusukebe)", + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + }, + "engines": { + "node": ">=18.14.1" + }, + "devDependencies": { + "@hono/eslint-config": "^1.0.1", + "@types/jest": "^29.5.3", + "@types/node": "^20.10.0", + "@types/supertest": "^2.0.12", + "@whatwg-node/fetch": "^0.9.14", + "eslint": "^9.10.0", + "hono": "^4.4.10", + "jest": "^29.6.1", + "np": "^7.7.0", + "prettier": "^3.2.4", + "publint": "^0.1.16", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", + "tsup": "^7.2.0", + "typescript": "^5.3.2" + }, + "peerDependencies": { + "hono": "^4" + }, + "packageManager": "bun@1.2.20" +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/LICENSE.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/LICENSE.txt new file mode 100644 index 00000000..c7e27478 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/LICENSE.txt @@ -0,0 +1,14 @@ +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/README.md new file mode 100644 index 00000000..48806426 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/README.md @@ -0,0 +1,143 @@ +# @isaacs/cliui + +Temporary fork of [cliui](http://npm.im/cliui). + +![ci](https://github.com/yargs/cliui/workflows/ci/badge.svg) +[![NPM version](https://img.shields.io/npm/v/cliui.svg)](https://www.npmjs.com/package/cliui) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) +![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/cliui) + +easily create complex multi-column command-line-interfaces. + +## Example + +```js +const ui = require('cliui')() + +ui.div('Usage: $0 [command] [options]') + +ui.div({ + text: 'Options:', + padding: [2, 0, 1, 0] +}) + +ui.div( + { + text: "-f, --file", + width: 20, + padding: [0, 4, 0, 4] + }, + { + text: "the file to load." + + chalk.green("(if this description is long it wraps).") + , + width: 20 + }, + { + text: chalk.red("[required]"), + align: 'right' + } +) + +console.log(ui.toString()) +``` + +## Deno/ESM Support + +As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and +[ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules): + +```typescript +import cliui from "https://deno.land/x/cliui/deno.ts"; + +const ui = cliui({}) + +ui.div('Usage: $0 [command] [options]') + +ui.div({ + text: 'Options:', + padding: [2, 0, 1, 0] +}) + +ui.div({ + text: "-f, --file", + width: 20, + padding: [0, 4, 0, 4] +}) + +console.log(ui.toString()) +``` + + + +## Layout DSL + +cliui exposes a simple layout DSL: + +If you create a single `ui.div`, passing a string rather than an +object: + +* `\n`: characters will be interpreted as new rows. +* `\t`: characters will be interpreted as new columns. +* `\s`: characters will be interpreted as padding. + +**as an example...** + +```js +var ui = require('./')({ + width: 60 +}) + +ui.div( + 'Usage: node ./bin/foo.js\n' + + ' \t provide a regex\n' + + ' \t provide a glob\t [required]' +) + +console.log(ui.toString()) +``` + +**will output:** + +```shell +Usage: node ./bin/foo.js + provide a regex + provide a glob [required] +``` + +## Methods + +```js +cliui = require('cliui') +``` + +### cliui({width: integer}) + +Specify the maximum width of the UI being generated. +If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`. + +### cliui({wrap: boolean}) + +Enable or disable the wrapping of text in a column. + +### cliui.div(column, column, column) + +Create a row with any number of columns, a column +can either be a string, or an object with the following +options: + +* **text:** some text to place in the column. +* **width:** the width of a column. +* **align:** alignment, `right` or `center`. +* **padding:** `[top, right, bottom, left]`. +* **border:** should a border be placed around the div? + +### cliui.span(column, column, column) + +Similar to `div`, except the next row will be appended without +a new line being created. + +### cliui.resetOutput() + +Resets the UI elements of the current cliui instance, maintaining the values +set for `width` and `wrap`. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/index.mjs b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/index.mjs new file mode 100644 index 00000000..5177519a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/index.mjs @@ -0,0 +1,14 @@ +// Bootstrap cliui with ESM dependencies: +import { cliui } from './build/lib/index.js' + +import stringWidth from 'string-width' +import stripAnsi from 'strip-ansi' +import wrap from 'wrap-ansi' + +export default function ui (opts) { + return cliui(opts, { + stringWidth, + stripAnsi, + wrap + }) +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/package.json new file mode 100644 index 00000000..7a952532 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/package.json @@ -0,0 +1,86 @@ +{ + "name": "@isaacs/cliui", + "version": "8.0.2", + "description": "easily create complex multi-column command-line-interfaces", + "main": "build/index.cjs", + "exports": { + ".": [ + { + "import": "./index.mjs", + "require": "./build/index.cjs" + }, + "./build/index.cjs" + ] + }, + "type": "module", + "module": "./index.mjs", + "scripts": { + "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'", + "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'", + "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", + "test": "c8 mocha ./test/*.cjs", + "test:esm": "c8 mocha ./test/**/*.mjs", + "postest": "check", + "coverage": "c8 report --check-coverage", + "precompile": "rimraf build", + "compile": "tsc", + "postcompile": "npm run build:cjs", + "build:cjs": "rollup -c", + "prepare": "npm run compile" + }, + "repository": "yargs/cliui", + "standard": { + "ignore": [ + "**/example/**" + ], + "globals": [ + "it" + ] + }, + "keywords": [ + "cli", + "command-line", + "layout", + "design", + "console", + "wrap", + "table" + ], + "author": "Ben Coe ", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "devDependencies": { + "@types/node": "^14.0.27", + "@typescript-eslint/eslint-plugin": "^4.0.0", + "@typescript-eslint/parser": "^4.0.0", + "c8": "^7.3.0", + "chai": "^4.2.0", + "chalk": "^4.1.0", + "cross-env": "^7.0.2", + "eslint": "^7.6.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-node": "^11.1.0", + "gts": "^3.0.0", + "mocha": "^10.0.0", + "rimraf": "^3.0.2", + "rollup": "^2.23.1", + "rollup-plugin-ts": "^3.0.2", + "standardx": "^7.0.0", + "typescript": "^4.0.0" + }, + "files": [ + "build", + "index.mjs", + "!*.d.ts" + ], + "engines": { + "node": ">=12" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/CHANGELOG.md new file mode 100644 index 00000000..afdc8350 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +### [0.1.3](https://github.com/istanbuljs/schema/compare/v0.1.2...v0.1.3) (2021-02-13) + + +### Features + +* Add `classPrivateMethods` and `topLevelAwait` default support ([#17](https://github.com/istanbuljs/schema/issues/17)) ([e732889](https://github.com/istanbuljs/schema/commit/e7328894ddeb61da256c1f13c2c2cc2e04f181df)), closes [#16](https://github.com/istanbuljs/schema/issues/16) +* Add `numericSeparator` to default `parserPlugins` ([#12](https://github.com/istanbuljs/schema/issues/12)) ([fe32f00](https://github.com/istanbuljs/schema/commit/fe32f002f54c61467b1c1a487081f51c85ec8d10)), closes [#5](https://github.com/istanbuljs/schema/issues/5) +* Add babel.config.mjs to default exclude ([#10](https://github.com/istanbuljs/schema/issues/10)) ([a4dbeaa](https://github.com/istanbuljs/schema/commit/a4dbeaa7045490a4d46754801ac71f5d99c9bd79)) + + +### Bug Fixes + +* Exclude tests with `tsx` or `jsx` extensions ([#13](https://github.com/istanbuljs/schema/issues/13)) ([c7747f7](https://github.com/istanbuljs/schema/commit/c7747f7a7df8a2b770036834af77dfd0ee445733)), closes [#11](https://github.com/istanbuljs/schema/issues/11) + +### [0.1.2](https://github.com/istanbuljs/schema/compare/v0.1.1...v0.1.2) (2019-12-05) + + +### Features + +* Ignore *.d.ts ([#6](https://github.com/istanbuljs/schema/issues/6)) ([d867eaf](https://github.com/istanbuljs/schema/commit/d867eaff6ca4abcd4301990e2bdcdf53e438e9c4)) +* Update default exclude of dev tool configurations ([#7](https://github.com/istanbuljs/schema/issues/7)) ([c89f818](https://github.com/istanbuljs/schema/commit/c89f8185f30879bcdf8d2f1c3b7aba0ac7056fa9)) + +## [0.1.1](https://github.com/istanbuljs/schema/compare/v0.1.0...v0.1.1) (2019-10-07) + + +### Bug Fixes + +* Add missing `instrument` option ([#3](https://github.com/istanbuljs/schema/issues/3)) ([bf1217d](https://github.com/istanbuljs/schema/commit/bf1217d)) + + +### Features + +* Add `use-spawn-wrap` nyc option ([#4](https://github.com/istanbuljs/schema/issues/4)) ([b2ce2e8](https://github.com/istanbuljs/schema/commit/b2ce2e8)) + +## 0.1.0 (2019-10-05) + + +### Features + +* Initial implementation ([99bd3a5](https://github.com/istanbuljs/schema/commit/99bd3a5)) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/LICENSE new file mode 100644 index 00000000..807a18bd --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 CFWare, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/README.md new file mode 100644 index 00000000..9cac0288 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/README.md @@ -0,0 +1,30 @@ +# @istanbuljs/schema + +[![Travis CI][travis-image]][travis-url] +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![MIT][license-image]](LICENSE) + +Schemas describing various structures used by nyc and istanbuljs + +## Usage + +```js +const {nyc} = require('@istanbuljs/schema').defaults; + +console.log(`Default exclude list:\n\t* ${nyc.exclude.join('\n\t* ')}`); +``` + +## `@istanbuljs/schema` for enterprise + +Available as part of the Tidelift Subscription. + +The maintainers of `@istanbuljs/schema` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-istanbuljs-schema?utm_source=npm-istanbuljs-schema&utm_medium=referral&utm_campaign=enterprise) + +[npm-image]: https://img.shields.io/npm/v/@istanbuljs/schema.svg +[npm-url]: https://npmjs.org/package/@istanbuljs/schema +[travis-image]: https://travis-ci.org/istanbuljs/schema.svg?branch=master +[travis-url]: https://travis-ci.org/istanbuljs/schema +[downloads-image]: https://img.shields.io/npm/dm/@istanbuljs/schema.svg +[downloads-url]: https://npmjs.org/package/@istanbuljs/schema +[license-image]: https://img.shields.io/npm/l/@istanbuljs/schema.svg diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-exclude.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-exclude.js new file mode 100644 index 00000000..c6bb5264 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-exclude.js @@ -0,0 +1,22 @@ +'use strict'; + +const defaultExtension = require('./default-extension.js'); +const testFileExtensions = defaultExtension + .map(extension => extension.slice(1)) + .join(','); + +module.exports = [ + 'coverage/**', + 'packages/*/test{,s}/**', + '**/*.d.ts', + 'test{,s}/**', + `test{,-*}.{${testFileExtensions}}`, + `**/*{.,-}test.{${testFileExtensions}}`, + '**/__tests__/**', + + /* Exclude common development tool configuration files */ + '**/{ava,babel,nyc}.config.{js,cjs,mjs}', + '**/jest.config.{js,cjs,mjs,ts}', + '**/{karma,rollup,webpack}.config.js', + '**/.{eslint,mocha}rc.{js,cjs}' +]; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-extension.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-extension.js new file mode 100644 index 00000000..46ebadca --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-extension.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = [ + '.js', + '.cjs', + '.mjs', + '.ts', + '.tsx', + '.jsx' +]; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/index.js new file mode 100644 index 00000000..b35f6101 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/index.js @@ -0,0 +1,466 @@ +'use strict'; + +const defaultExclude = require('./default-exclude.js'); +const defaultExtension = require('./default-extension.js'); + +const nycCommands = { + all: [null, 'check-coverage', 'instrument', 'merge', 'report'], + testExclude: [null, 'instrument', 'report', 'check-coverage'], + instrument: [null, 'instrument'], + checkCoverage: [null, 'report', 'check-coverage'], + report: [null, 'report'], + main: [null], + instrumentOnly: ['instrument'] +}; + +const cwd = { + description: 'working directory used when resolving paths', + type: 'string', + get default() { + return process.cwd(); + }, + nycCommands: nycCommands.all +}; + +const nycrcPath = { + description: 'specify an explicit path to find nyc configuration', + nycCommands: nycCommands.all +}; + +const tempDir = { + description: 'directory to output raw coverage information to', + type: 'string', + default: './.nyc_output', + nycAlias: 't', + nycHiddenAlias: 'temp-directory', + nycCommands: [null, 'check-coverage', 'merge', 'report'] +}; + +const testExclude = { + exclude: { + description: 'a list of specific files and directories that should be excluded from coverage, glob patterns are supported', + type: 'array', + items: { + type: 'string' + }, + default: defaultExclude, + nycCommands: nycCommands.testExclude, + nycAlias: 'x' + }, + excludeNodeModules: { + description: 'whether or not to exclude all node_module folders (i.e. **/node_modules/**) by default', + type: 'boolean', + default: true, + nycCommands: nycCommands.testExclude + }, + include: { + description: 'a list of specific files that should be covered, glob patterns are supported', + type: 'array', + items: { + type: 'string' + }, + default: [], + nycCommands: nycCommands.testExclude, + nycAlias: 'n' + }, + extension: { + description: 'a list of extensions that nyc should handle in addition to .js', + type: 'array', + items: { + type: 'string' + }, + default: defaultExtension, + nycCommands: nycCommands.testExclude, + nycAlias: 'e' + } +}; + +const instrumentVisitor = { + coverageVariable: { + description: 'variable to store coverage', + type: 'string', + default: '__coverage__', + nycCommands: nycCommands.instrument + }, + coverageGlobalScope: { + description: 'scope to store the coverage variable', + type: 'string', + default: 'this', + nycCommands: nycCommands.instrument + }, + coverageGlobalScopeFunc: { + description: 'avoid potentially replaced `Function` when finding global scope', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + }, + ignoreClassMethods: { + description: 'class method names to ignore for coverage', + type: 'array', + items: { + type: 'string' + }, + default: [], + nycCommands: nycCommands.instrument + } +}; + +const instrumentParseGen = { + autoWrap: { + description: 'allow `return` statements outside of functions', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + }, + esModules: { + description: 'should files be treated as ES Modules', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + }, + parserPlugins: { + description: 'babel parser plugins to use when parsing the source', + type: 'array', + items: { + type: 'string' + }, + /* Babel parser plugins are to be enabled when the feature is stage 3 and + * implemented in a released version of node.js. */ + default: [ + 'asyncGenerators', + 'bigInt', + 'classProperties', + 'classPrivateProperties', + 'classPrivateMethods', + 'dynamicImport', + 'importMeta', + 'numericSeparator', + 'objectRestSpread', + 'optionalCatchBinding', + 'topLevelAwait' + ], + nycCommands: nycCommands.instrument + }, + compact: { + description: 'should the output be compacted?', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + }, + preserveComments: { + description: 'should comments be preserved in the output?', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + }, + produceSourceMap: { + description: 'should source maps be produced?', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + } +}; + +const checkCoverage = { + excludeAfterRemap: { + description: 'should exclude logic be performed after the source-map remaps filenames?', + type: 'boolean', + default: true, + nycCommands: nycCommands.checkCoverage + }, + branches: { + description: 'what % of branches must be covered?', + type: 'number', + default: 0, + minimum: 0, + maximum: 100, + nycCommands: nycCommands.checkCoverage + }, + functions: { + description: 'what % of functions must be covered?', + type: 'number', + default: 0, + minimum: 0, + maximum: 100, + nycCommands: nycCommands.checkCoverage + }, + lines: { + description: 'what % of lines must be covered?', + type: 'number', + default: 90, + minimum: 0, + maximum: 100, + nycCommands: nycCommands.checkCoverage + }, + statements: { + description: 'what % of statements must be covered?', + type: 'number', + default: 0, + minimum: 0, + maximum: 100, + nycCommands: nycCommands.checkCoverage + }, + perFile: { + description: 'check thresholds per file', + type: 'boolean', + default: false, + nycCommands: nycCommands.checkCoverage + } +}; + +const report = { + checkCoverage: { + description: 'check whether coverage is within thresholds provided', + type: 'boolean', + default: false, + nycCommands: nycCommands.report + }, + reporter: { + description: 'coverage reporter(s) to use', + type: 'array', + items: { + type: 'string' + }, + default: ['text'], + nycCommands: nycCommands.report, + nycAlias: 'r' + }, + reportDir: { + description: 'directory to output coverage reports in', + type: 'string', + default: 'coverage', + nycCommands: nycCommands.report + }, + showProcessTree: { + description: 'display the tree of spawned processes', + type: 'boolean', + default: false, + nycCommands: nycCommands.report + }, + skipEmpty: { + description: 'don\'t show empty files (no lines of code) in report', + type: 'boolean', + default: false, + nycCommands: nycCommands.report + }, + skipFull: { + description: 'don\'t show files with 100% statement, branch, and function coverage', + type: 'boolean', + default: false, + nycCommands: nycCommands.report + } +}; + +const nycMain = { + silent: { + description: 'don\'t output a report after tests finish running', + type: 'boolean', + default: false, + nycCommands: nycCommands.main, + nycAlias: 's' + }, + all: { + description: 'whether or not to instrument all files of the project (not just the ones touched by your test suite)', + type: 'boolean', + default: false, + nycCommands: nycCommands.main, + nycAlias: 'a' + }, + eager: { + description: 'instantiate the instrumenter at startup (see https://git.io/vMKZ9)', + type: 'boolean', + default: false, + nycCommands: nycCommands.main + }, + cache: { + description: 'cache instrumentation results for improved performance', + type: 'boolean', + default: true, + nycCommands: nycCommands.main, + nycAlias: 'c' + }, + cacheDir: { + description: 'explicitly set location for instrumentation cache', + type: 'string', + nycCommands: nycCommands.main + }, + babelCache: { + description: 'cache babel transpilation results for improved performance', + type: 'boolean', + default: false, + nycCommands: nycCommands.main + }, + useSpawnWrap: { + description: 'use spawn-wrap instead of setting process.env.NODE_OPTIONS', + type: 'boolean', + default: false, + nycCommands: nycCommands.main + }, + hookRequire: { + description: 'should nyc wrap require?', + type: 'boolean', + default: true, + nycCommands: nycCommands.main + }, + hookRunInContext: { + description: 'should nyc wrap vm.runInContext?', + type: 'boolean', + default: false, + nycCommands: nycCommands.main + }, + hookRunInThisContext: { + description: 'should nyc wrap vm.runInThisContext?', + type: 'boolean', + default: false, + nycCommands: nycCommands.main + }, + clean: { + description: 'should the .nyc_output folder be cleaned before executing tests', + type: 'boolean', + default: true, + nycCommands: nycCommands.main + } +}; + +const instrumentOnly = { + inPlace: { + description: 'should nyc run the instrumentation in place?', + type: 'boolean', + default: false, + nycCommands: nycCommands.instrumentOnly + }, + exitOnError: { + description: 'should nyc exit when an instrumentation failure occurs?', + type: 'boolean', + default: false, + nycCommands: nycCommands.instrumentOnly + }, + delete: { + description: 'should the output folder be deleted before instrumenting files?', + type: 'boolean', + default: false, + nycCommands: nycCommands.instrumentOnly + }, + completeCopy: { + description: 'should nyc copy all files from input to output as well as instrumented files?', + type: 'boolean', + default: false, + nycCommands: nycCommands.instrumentOnly + } +}; + +const nyc = { + description: 'nyc configuration options', + type: 'object', + properties: { + cwd, + nycrcPath, + tempDir, + + /* Test Exclude */ + ...testExclude, + + /* Instrumentation settings */ + ...instrumentVisitor, + + /* Instrumentation parser/generator settings */ + ...instrumentParseGen, + sourceMap: { + description: 'should nyc detect and handle source maps?', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + }, + require: { + description: 'a list of additional modules that nyc should attempt to require in its subprocess, e.g., @babel/register, @babel/polyfill', + type: 'array', + items: { + type: 'string' + }, + default: [], + nycCommands: nycCommands.instrument, + nycAlias: 'i' + }, + instrument: { + description: 'should nyc handle instrumentation?', + type: 'boolean', + default: true, + nycCommands: nycCommands.instrument + }, + + /* Check coverage */ + ...checkCoverage, + + /* Report options */ + ...report, + + /* Main command options */ + ...nycMain, + + /* Instrument command options */ + ...instrumentOnly + } +}; + +const configs = { + nyc, + testExclude: { + description: 'test-exclude options', + type: 'object', + properties: { + cwd, + ...testExclude + } + }, + babelPluginIstanbul: { + description: 'babel-plugin-istanbul options', + type: 'object', + properties: { + cwd, + ...testExclude, + ...instrumentVisitor + } + }, + instrumentVisitor: { + description: 'instrument visitor options', + type: 'object', + properties: instrumentVisitor + }, + instrumenter: { + description: 'stand-alone instrumenter options', + type: 'object', + properties: { + ...instrumentVisitor, + ...instrumentParseGen + } + } +}; + +function defaultsReducer(defaults, [name, {default: value}]) { + /* Modifying arrays in defaults is safe, does not change schema. */ + if (Array.isArray(value)) { + value = [...value]; + } + + return Object.assign(defaults, {[name]: value}); +} + +module.exports = { + ...configs, + defaults: Object.keys(configs).reduce( + (defaults, id) => { + Object.defineProperty(defaults, id, { + enumerable: true, + get() { + /* This defers `process.cwd()` until defaults are requested. */ + return Object.entries(configs[id].properties) + .filter(([, info]) => 'default' in info) + .reduce(defaultsReducer, {}); + } + }); + + return defaults; + }, + {} + ) +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/package.json new file mode 100644 index 00000000..1d22cde9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/package.json @@ -0,0 +1,30 @@ +{ + "name": "@istanbuljs/schema", + "version": "0.1.3", + "description": "Schemas describing various structures used by nyc and istanbuljs", + "main": "index.js", + "scripts": { + "release": "standard-version --sign", + "pretest": "xo", + "test": "tap", + "snap": "npm test -- --snapshot" + }, + "engines": { + "node": ">=8" + }, + "author": "Corey Farrell", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/istanbuljs/schema.git" + }, + "bugs": { + "url": "https://github.com/istanbuljs/schema/issues" + }, + "homepage": "https://github.com/istanbuljs/schema#readme", + "devDependencies": { + "standard-version": "^7.0.0", + "tap": "^14.6.7", + "xo": "^0.25.3" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/LICENSE new file mode 100644 index 00000000..1f6ce94c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/LICENSE @@ -0,0 +1,19 @@ +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/README.md new file mode 100644 index 00000000..93692b10 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/README.md @@ -0,0 +1,227 @@ +# @jridgewell/gen-mapping + +> Generate source maps + +`gen-mapping` allows you to generate a source map during transpilation or minification. +With a source map, you're able to trace the original location in the source file, either in Chrome's +DevTools or using a library like [`@jridgewell/trace-mapping`][trace-mapping]. + +You may already be familiar with the [`source-map`][source-map] package's `SourceMapGenerator`. This +provides the same `addMapping` and `setSourceContent` API. + +## Installation + +```sh +npm install @jridgewell/gen-mapping +``` + +## Usage + +```typescript +import { GenMapping, addMapping, setSourceContent, toEncodedMap, toDecodedMap } from '@jridgewell/gen-mapping'; + +const map = new GenMapping({ + file: 'output.js', + sourceRoot: 'https://example.com/', +}); + +setSourceContent(map, 'input.js', `function foo() {}`); + +addMapping(map, { + // Lines start at line 1, columns at column 0. + generated: { line: 1, column: 0 }, + source: 'input.js', + original: { line: 1, column: 0 }, +}); + +addMapping(map, { + generated: { line: 1, column: 9 }, + source: 'input.js', + original: { line: 1, column: 9 }, + name: 'foo', +}); + +assert.deepEqual(toDecodedMap(map), { + version: 3, + file: 'output.js', + names: ['foo'], + sourceRoot: 'https://example.com/', + sources: ['input.js'], + sourcesContent: ['function foo() {}'], + mappings: [ + [ [0, 0, 0, 0], [9, 0, 0, 9, 0] ] + ], +}); + +assert.deepEqual(toEncodedMap(map), { + version: 3, + file: 'output.js', + names: ['foo'], + sourceRoot: 'https://example.com/', + sources: ['input.js'], + sourcesContent: ['function foo() {}'], + mappings: 'AAAA,SAASA', +}); +``` + +### Smaller Sourcemaps + +Not everything needs to be added to a sourcemap, and needless markings can cause signficantly +larger file sizes. `gen-mapping` exposes `maybeAddSegment`/`maybeAddMapping` APIs that will +intelligently determine if this marking adds useful information. If not, the marking will be +skipped. + +```typescript +import { maybeAddMapping } from '@jridgewell/gen-mapping'; + +const map = new GenMapping(); + +// Adding a sourceless marking at the beginning of a line isn't useful. +maybeAddMapping(map, { + generated: { line: 1, column: 0 }, +}); + +// Adding a new source marking is useful. +maybeAddMapping(map, { + generated: { line: 1, column: 0 }, + source: 'input.js', + original: { line: 1, column: 0 }, +}); + +// But adding another marking pointing to the exact same original location isn't, even if the +// generated column changed. +maybeAddMapping(map, { + generated: { line: 1, column: 9 }, + source: 'input.js', + original: { line: 1, column: 0 }, +}); + +assert.deepEqual(toEncodedMap(map), { + version: 3, + names: [], + sources: ['input.js'], + sourcesContent: [null], + mappings: 'AAAA', +}); +``` + +## Benchmarks + +``` +node v18.0.0 + +amp.js.map +Memory Usage: +gen-mapping: addSegment 5852872 bytes +gen-mapping: addMapping 7716042 bytes +source-map-js 6143250 bytes +source-map-0.6.1 6124102 bytes +source-map-0.8.0 6121173 bytes +Smallest memory usage is gen-mapping: addSegment + +Adding speed: +gen-mapping: addSegment x 441 ops/sec ±2.07% (90 runs sampled) +gen-mapping: addMapping x 350 ops/sec ±2.40% (86 runs sampled) +source-map-js: addMapping x 169 ops/sec ±2.42% (80 runs sampled) +source-map-0.6.1: addMapping x 167 ops/sec ±2.56% (80 runs sampled) +source-map-0.8.0: addMapping x 168 ops/sec ±2.52% (80 runs sampled) +Fastest is gen-mapping: addSegment + +Generate speed: +gen-mapping: decoded output x 150,824,370 ops/sec ±0.07% (102 runs sampled) +gen-mapping: encoded output x 663 ops/sec ±0.22% (98 runs sampled) +source-map-js: encoded output x 197 ops/sec ±0.45% (84 runs sampled) +source-map-0.6.1: encoded output x 198 ops/sec ±0.33% (85 runs sampled) +source-map-0.8.0: encoded output x 197 ops/sec ±0.06% (93 runs sampled) +Fastest is gen-mapping: decoded output + + +*** + + +babel.min.js.map +Memory Usage: +gen-mapping: addSegment 37578063 bytes +gen-mapping: addMapping 37212897 bytes +source-map-js 47638527 bytes +source-map-0.6.1 47690503 bytes +source-map-0.8.0 47470188 bytes +Smallest memory usage is gen-mapping: addMapping + +Adding speed: +gen-mapping: addSegment x 31.05 ops/sec ±8.31% (43 runs sampled) +gen-mapping: addMapping x 29.83 ops/sec ±7.36% (51 runs sampled) +source-map-js: addMapping x 20.73 ops/sec ±6.22% (38 runs sampled) +source-map-0.6.1: addMapping x 20.03 ops/sec ±10.51% (38 runs sampled) +source-map-0.8.0: addMapping x 19.30 ops/sec ±8.27% (37 runs sampled) +Fastest is gen-mapping: addSegment + +Generate speed: +gen-mapping: decoded output x 381,379,234 ops/sec ±0.29% (96 runs sampled) +gen-mapping: encoded output x 95.15 ops/sec ±2.98% (72 runs sampled) +source-map-js: encoded output x 15.20 ops/sec ±7.41% (33 runs sampled) +source-map-0.6.1: encoded output x 16.36 ops/sec ±10.46% (31 runs sampled) +source-map-0.8.0: encoded output x 16.06 ops/sec ±6.45% (31 runs sampled) +Fastest is gen-mapping: decoded output + + +*** + + +preact.js.map +Memory Usage: +gen-mapping: addSegment 416247 bytes +gen-mapping: addMapping 419824 bytes +source-map-js 1024619 bytes +source-map-0.6.1 1146004 bytes +source-map-0.8.0 1113250 bytes +Smallest memory usage is gen-mapping: addSegment + +Adding speed: +gen-mapping: addSegment x 13,755 ops/sec ±0.15% (98 runs sampled) +gen-mapping: addMapping x 13,013 ops/sec ±0.11% (101 runs sampled) +source-map-js: addMapping x 4,564 ops/sec ±0.21% (98 runs sampled) +source-map-0.6.1: addMapping x 4,562 ops/sec ±0.11% (99 runs sampled) +source-map-0.8.0: addMapping x 4,593 ops/sec ±0.11% (100 runs sampled) +Fastest is gen-mapping: addSegment + +Generate speed: +gen-mapping: decoded output x 379,864,020 ops/sec ±0.23% (93 runs sampled) +gen-mapping: encoded output x 14,368 ops/sec ±4.07% (82 runs sampled) +source-map-js: encoded output x 5,261 ops/sec ±0.21% (99 runs sampled) +source-map-0.6.1: encoded output x 5,124 ops/sec ±0.58% (99 runs sampled) +source-map-0.8.0: encoded output x 5,434 ops/sec ±0.33% (96 runs sampled) +Fastest is gen-mapping: decoded output + + +*** + + +react.js.map +Memory Usage: +gen-mapping: addSegment 975096 bytes +gen-mapping: addMapping 1102981 bytes +source-map-js 2918836 bytes +source-map-0.6.1 2885435 bytes +source-map-0.8.0 2874336 bytes +Smallest memory usage is gen-mapping: addSegment + +Adding speed: +gen-mapping: addSegment x 4,772 ops/sec ±0.15% (100 runs sampled) +gen-mapping: addMapping x 4,456 ops/sec ±0.13% (97 runs sampled) +source-map-js: addMapping x 1,618 ops/sec ±0.24% (97 runs sampled) +source-map-0.6.1: addMapping x 1,622 ops/sec ±0.12% (99 runs sampled) +source-map-0.8.0: addMapping x 1,631 ops/sec ±0.12% (100 runs sampled) +Fastest is gen-mapping: addSegment + +Generate speed: +gen-mapping: decoded output x 379,107,695 ops/sec ±0.07% (99 runs sampled) +gen-mapping: encoded output x 5,421 ops/sec ±1.60% (89 runs sampled) +source-map-js: encoded output x 2,113 ops/sec ±1.81% (98 runs sampled) +source-map-0.6.1: encoded output x 2,126 ops/sec ±0.10% (100 runs sampled) +source-map-0.8.0: encoded output x 2,176 ops/sec ±0.39% (98 runs sampled) +Fastest is gen-mapping: decoded output +``` + +[source-map]: https://www.npmjs.com/package/source-map +[trace-mapping]: https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/package.json new file mode 100644 index 00000000..036f9b79 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/package.json @@ -0,0 +1,67 @@ +{ + "name": "@jridgewell/gen-mapping", + "version": "0.3.13", + "description": "Generate source maps", + "keywords": [ + "source", + "map" + ], + "main": "dist/gen-mapping.umd.js", + "module": "dist/gen-mapping.mjs", + "types": "types/gen-mapping.d.cts", + "files": [ + "dist", + "src", + "types" + ], + "exports": { + ".": [ + { + "import": { + "types": "./types/gen-mapping.d.mts", + "default": "./dist/gen-mapping.mjs" + }, + "default": { + "types": "./types/gen-mapping.d.cts", + "default": "./dist/gen-mapping.umd.js" + } + }, + "./dist/gen-mapping.umd.js" + ], + "./package.json": "./package.json" + }, + "scripts": { + "benchmark": "run-s build:code benchmark:*", + "benchmark:install": "cd benchmark && npm install", + "benchmark:only": "node --expose-gc benchmark/index.js", + "build": "run-s -n build:code build:types", + "build:code": "node ../../esbuild.mjs gen-mapping.ts", + "build:types": "run-s build:types:force build:types:emit build:types:mts", + "build:types:force": "rimraf tsconfig.build.tsbuildinfo", + "build:types:emit": "tsc --project tsconfig.build.json", + "build:types:mts": "node ../../mts-types.mjs", + "clean": "run-s -n clean:code clean:types", + "clean:code": "tsc --build --clean tsconfig.build.json", + "clean:types": "rimraf dist types", + "test": "run-s -n test:types test:only test:format", + "test:format": "prettier --check '{src,test}/**/*.ts'", + "test:only": "mocha", + "test:types": "eslint '{src,test}/**/*.ts'", + "lint": "run-s -n lint:types lint:format", + "lint:format": "npm run test:format -- --write", + "lint:types": "npm run test:types -- --fix", + "prepublishOnly": "npm run-s -n build test" + }, + "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping", + "repository": { + "type": "git", + "url": "git+https://github.com/jridgewell/sourcemaps.git", + "directory": "packages/gen-mapping" + }, + "author": "Justin Ridgewell ", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/gen-mapping.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/gen-mapping.ts new file mode 100644 index 00000000..ecc878c5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/gen-mapping.ts @@ -0,0 +1,614 @@ +import { SetArray, put, remove } from './set-array'; +import { + encode, + // encodeGeneratedRanges, + // encodeOriginalScopes +} from '@jridgewell/sourcemap-codec'; +import { TraceMap, decodedMappings } from '@jridgewell/trace-mapping'; + +import { + COLUMN, + SOURCES_INDEX, + SOURCE_LINE, + SOURCE_COLUMN, + NAMES_INDEX, +} from './sourcemap-segment'; + +import type { SourceMapInput } from '@jridgewell/trace-mapping'; +// import type { OriginalScope, GeneratedRange } from '@jridgewell/sourcemap-codec'; +import type { SourceMapSegment } from './sourcemap-segment'; +import type { + DecodedSourceMap, + EncodedSourceMap, + Pos, + Mapping, + // BindingExpressionRange, + // OriginalPos, + // OriginalScopeInfo, + // GeneratedRangeInfo, +} from './types'; + +export type { DecodedSourceMap, EncodedSourceMap, Mapping }; + +export type Options = { + file?: string | null; + sourceRoot?: string | null; +}; + +const NO_NAME = -1; + +/** + * Provides the state to generate a sourcemap. + */ +export class GenMapping { + declare private _names: SetArray; + declare private _sources: SetArray; + declare private _sourcesContent: (string | null)[]; + declare private _mappings: SourceMapSegment[][]; + // private declare _originalScopes: OriginalScope[][]; + // private declare _generatedRanges: GeneratedRange[]; + declare private _ignoreList: SetArray; + declare file: string | null | undefined; + declare sourceRoot: string | null | undefined; + + constructor({ file, sourceRoot }: Options = {}) { + this._names = new SetArray(); + this._sources = new SetArray(); + this._sourcesContent = []; + this._mappings = []; + // this._originalScopes = []; + // this._generatedRanges = []; + this.file = file; + this.sourceRoot = sourceRoot; + this._ignoreList = new SetArray(); + } +} + +interface PublicMap { + _names: GenMapping['_names']; + _sources: GenMapping['_sources']; + _sourcesContent: GenMapping['_sourcesContent']; + _mappings: GenMapping['_mappings']; + // _originalScopes: GenMapping['_originalScopes']; + // _generatedRanges: GenMapping['_generatedRanges']; + _ignoreList: GenMapping['_ignoreList']; +} + +/** + * Typescript doesn't allow friend access to private fields, so this just casts the map into a type + * with public access modifiers. + */ +function cast(map: unknown): PublicMap { + return map as any; +} + +/** + * A low-level API to associate a generated position with an original source position. Line and + * column here are 0-based, unlike `addMapping`. + */ +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source?: null, + sourceLine?: null, + sourceColumn?: null, + name?: null, + content?: null, +): void; +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source: string, + sourceLine: number, + sourceColumn: number, + name?: null, + content?: string | null, +): void; +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source: string, + sourceLine: number, + sourceColumn: number, + name: string, + content?: string | null, +): void; +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source?: string | null, + sourceLine?: number | null, + sourceColumn?: number | null, + name?: string | null, + content?: string | null, +): void { + return addSegmentInternal( + false, + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + content, + ); +} + +/** + * A high-level API to associate a generated position with an original source position. Line is + * 1-based, but column is 0-based, due to legacy behavior in `source-map` library. + */ +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source?: null; + original?: null; + name?: null; + content?: null; + }, +): void; +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source: string; + original: Pos; + name?: null; + content?: string | null; + }, +): void; +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source: string; + original: Pos; + name: string; + content?: string | null; + }, +): void; +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source?: string | null; + original?: Pos | null; + name?: string | null; + content?: string | null; + }, +): void { + return addMappingInternal(false, map, mapping as Parameters[2]); +} + +/** + * Same as `addSegment`, but will only add the segment if it generates useful information in the + * resulting map. This only works correctly if segments are added **in order**, meaning you should + * not add a segment with a lower generated line/column than one that came before. + */ +export const maybeAddSegment: typeof addSegment = ( + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + content, +) => { + return addSegmentInternal( + true, + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + content, + ); +}; + +/** + * Same as `addMapping`, but will only add the mapping if it generates useful information in the + * resulting map. This only works correctly if mappings are added **in order**, meaning you should + * not add a mapping with a lower generated line/column than one that came before. + */ +export const maybeAddMapping: typeof addMapping = (map, mapping) => { + return addMappingInternal(true, map, mapping as Parameters[2]); +}; + +/** + * Adds/removes the content of the source file to the source map. + */ +export function setSourceContent(map: GenMapping, source: string, content: string | null): void { + const { + _sources: sources, + _sourcesContent: sourcesContent, + // _originalScopes: originalScopes, + } = cast(map); + const index = put(sources, source); + sourcesContent[index] = content; + // if (index === originalScopes.length) originalScopes[index] = []; +} + +export function setIgnore(map: GenMapping, source: string, ignore = true) { + const { + _sources: sources, + _sourcesContent: sourcesContent, + _ignoreList: ignoreList, + // _originalScopes: originalScopes, + } = cast(map); + const index = put(sources, source); + if (index === sourcesContent.length) sourcesContent[index] = null; + // if (index === originalScopes.length) originalScopes[index] = []; + if (ignore) put(ignoreList, index); + else remove(ignoreList, index); +} + +/** + * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export function toDecodedMap(map: GenMapping): DecodedSourceMap { + const { + _mappings: mappings, + _sources: sources, + _sourcesContent: sourcesContent, + _names: names, + _ignoreList: ignoreList, + // _originalScopes: originalScopes, + // _generatedRanges: generatedRanges, + } = cast(map); + removeEmptyFinalLines(mappings); + + return { + version: 3, + file: map.file || undefined, + names: names.array, + sourceRoot: map.sourceRoot || undefined, + sources: sources.array, + sourcesContent, + mappings, + // originalScopes, + // generatedRanges, + ignoreList: ignoreList.array, + }; +} + +/** + * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export function toEncodedMap(map: GenMapping): EncodedSourceMap { + const decoded = toDecodedMap(map); + return Object.assign({}, decoded, { + // originalScopes: decoded.originalScopes.map((os) => encodeOriginalScopes(os)), + // generatedRanges: encodeGeneratedRanges(decoded.generatedRanges as GeneratedRange[]), + mappings: encode(decoded.mappings as SourceMapSegment[][]), + }); +} + +/** + * Constructs a new GenMapping, using the already present mappings of the input. + */ +export function fromMap(input: SourceMapInput): GenMapping { + const map = new TraceMap(input); + const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot }); + + putAll(cast(gen)._names, map.names); + putAll(cast(gen)._sources, map.sources as string[]); + cast(gen)._sourcesContent = map.sourcesContent || map.sources.map(() => null); + cast(gen)._mappings = decodedMappings(map) as GenMapping['_mappings']; + // TODO: implement originalScopes/generatedRanges + if (map.ignoreList) putAll(cast(gen)._ignoreList, map.ignoreList); + + return gen; +} + +/** + * Returns an array of high-level mapping objects for every recorded segment, which could then be + * passed to the `source-map` library. + */ +export function allMappings(map: GenMapping): Mapping[] { + const out: Mapping[] = []; + const { _mappings: mappings, _sources: sources, _names: names } = cast(map); + + for (let i = 0; i < mappings.length; i++) { + const line = mappings[i]; + for (let j = 0; j < line.length; j++) { + const seg = line[j]; + + const generated = { line: i + 1, column: seg[COLUMN] }; + let source: string | undefined = undefined; + let original: Pos | undefined = undefined; + let name: string | undefined = undefined; + + if (seg.length !== 1) { + source = sources.array[seg[SOURCES_INDEX]]; + original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] }; + + if (seg.length === 5) name = names.array[seg[NAMES_INDEX]]; + } + + out.push({ generated, source, original, name } as Mapping); + } + } + + return out; +} + +// This split declaration is only so that terser can elminiate the static initialization block. +function addSegmentInternal( + skipable: boolean, + map: GenMapping, + genLine: number, + genColumn: number, + source: S, + sourceLine: S extends string ? number : null | undefined, + sourceColumn: S extends string ? number : null | undefined, + name: S extends string ? string | null | undefined : null | undefined, + content: S extends string ? string | null | undefined : null | undefined, +): void { + const { + _mappings: mappings, + _sources: sources, + _sourcesContent: sourcesContent, + _names: names, + // _originalScopes: originalScopes, + } = cast(map); + const line = getIndex(mappings, genLine); + const index = getColumnIndex(line, genColumn); + + if (!source) { + if (skipable && skipSourceless(line, index)) return; + return insert(line, index, [genColumn]); + } + + // Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source + // isn't nullish. + assert(sourceLine); + assert(sourceColumn); + + const sourcesIndex = put(sources, source); + const namesIndex = name ? put(names, name) : NO_NAME; + if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content ?? null; + // if (sourcesIndex === originalScopes.length) originalScopes[sourcesIndex] = []; + + if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) { + return; + } + + return insert( + line, + index, + name + ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] + : [genColumn, sourcesIndex, sourceLine, sourceColumn], + ); +} + +function assert(_val: unknown): asserts _val is T { + // noop. +} + +function getIndex(arr: T[][], index: number): T[] { + for (let i = arr.length; i <= index; i++) { + arr[i] = []; + } + return arr[index]; +} + +function getColumnIndex(line: SourceMapSegment[], genColumn: number): number { + let index = line.length; + for (let i = index - 1; i >= 0; index = i--) { + const current = line[i]; + if (genColumn >= current[COLUMN]) break; + } + return index; +} + +function insert(array: T[], index: number, value: T) { + for (let i = array.length; i > index; i--) { + array[i] = array[i - 1]; + } + array[index] = value; +} + +function removeEmptyFinalLines(mappings: SourceMapSegment[][]) { + const { length } = mappings; + let len = length; + for (let i = len - 1; i >= 0; len = i, i--) { + if (mappings[i].length > 0) break; + } + if (len < length) mappings.length = len; +} + +function putAll(setarr: SetArray, array: T[]) { + for (let i = 0; i < array.length; i++) put(setarr, array[i]); +} + +function skipSourceless(line: SourceMapSegment[], index: number): boolean { + // The start of a line is already sourceless, so adding a sourceless segment to the beginning + // doesn't generate any useful information. + if (index === 0) return true; + + const prev = line[index - 1]; + // If the previous segment is also sourceless, then adding another sourceless segment doesn't + // genrate any new information. Else, this segment will end the source/named segment and point to + // a sourceless position, which is useful. + return prev.length === 1; +} + +function skipSource( + line: SourceMapSegment[], + index: number, + sourcesIndex: number, + sourceLine: number, + sourceColumn: number, + namesIndex: number, +): boolean { + // A source/named segment at the start of a line gives position at that genColumn + if (index === 0) return false; + + const prev = line[index - 1]; + + // If the previous segment is sourceless, then we're transitioning to a source. + if (prev.length === 1) return false; + + // If the previous segment maps to the exact same source position, then this segment doesn't + // provide any new position information. + return ( + sourcesIndex === prev[SOURCES_INDEX] && + sourceLine === prev[SOURCE_LINE] && + sourceColumn === prev[SOURCE_COLUMN] && + namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME) + ); +} + +function addMappingInternal( + skipable: boolean, + map: GenMapping, + mapping: { + generated: Pos; + source: S; + original: S extends string ? Pos : null | undefined; + name: S extends string ? string | null | undefined : null | undefined; + content: S extends string ? string | null | undefined : null | undefined; + }, +) { + const { generated, source, original, name, content } = mapping; + if (!source) { + return addSegmentInternal( + skipable, + map, + generated.line - 1, + generated.column, + null, + null, + null, + null, + null, + ); + } + assert(original); + return addSegmentInternal( + skipable, + map, + generated.line - 1, + generated.column, + source as string, + original.line - 1, + original.column, + name, + content, + ); +} + +/* +export function addOriginalScope( + map: GenMapping, + data: { + start: Pos; + end: Pos; + source: string; + kind: string; + name?: string; + variables?: string[]; + }, +): OriginalScopeInfo { + const { start, end, source, kind, name, variables } = data; + const { + _sources: sources, + _sourcesContent: sourcesContent, + _originalScopes: originalScopes, + _names: names, + } = cast(map); + const index = put(sources, source); + if (index === sourcesContent.length) sourcesContent[index] = null; + if (index === originalScopes.length) originalScopes[index] = []; + + const kindIndex = put(names, kind); + const scope: OriginalScope = name + ? [start.line - 1, start.column, end.line - 1, end.column, kindIndex, put(names, name)] + : [start.line - 1, start.column, end.line - 1, end.column, kindIndex]; + if (variables) { + scope.vars = variables.map((v) => put(names, v)); + } + const len = originalScopes[index].push(scope); + return [index, len - 1, variables]; +} +*/ + +// Generated Ranges +/* +export function addGeneratedRange( + map: GenMapping, + data: { + start: Pos; + isScope: boolean; + originalScope?: OriginalScopeInfo; + callsite?: OriginalPos; + }, +): GeneratedRangeInfo { + const { start, isScope, originalScope, callsite } = data; + const { + _originalScopes: originalScopes, + _sources: sources, + _sourcesContent: sourcesContent, + _generatedRanges: generatedRanges, + } = cast(map); + + const range: GeneratedRange = [ + start.line - 1, + start.column, + 0, + 0, + originalScope ? originalScope[0] : -1, + originalScope ? originalScope[1] : -1, + ]; + if (originalScope?.[2]) { + range.bindings = originalScope[2].map(() => [[-1]]); + } + if (callsite) { + const index = put(sources, callsite.source); + if (index === sourcesContent.length) sourcesContent[index] = null; + if (index === originalScopes.length) originalScopes[index] = []; + range.callsite = [index, callsite.line - 1, callsite.column]; + } + if (isScope) range.isScope = true; + generatedRanges.push(range); + + return [range, originalScope?.[2]]; +} + +export function setEndPosition(range: GeneratedRangeInfo, pos: Pos) { + range[0][2] = pos.line - 1; + range[0][3] = pos.column; +} + +export function addBinding( + map: GenMapping, + range: GeneratedRangeInfo, + variable: string, + expression: string | BindingExpressionRange, +) { + const { _names: names } = cast(map); + const bindings = (range[0].bindings ||= []); + const vars = range[1]; + + const index = vars!.indexOf(variable); + const binding = getIndex(bindings, index); + + if (typeof expression === 'string') binding[0] = [put(names, expression)]; + else { + const { start } = expression; + binding.push([put(names, expression.expression), start.line - 1, start.column]); + } +} +*/ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/set-array.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/set-array.ts new file mode 100644 index 00000000..a2a73a52 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/set-array.ts @@ -0,0 +1,82 @@ +type Key = string | number | symbol; + +/** + * SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the + * index of the `key` in the backing array. + * + * This is designed to allow synchronizing a second array with the contents of the backing array, + * like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`, + * and there are never duplicates. + */ +export class SetArray { + declare private _indexes: Record; + declare array: readonly T[]; + + constructor() { + this._indexes = { __proto__: null } as any; + this.array = []; + } +} + +interface PublicSet { + array: T[]; + _indexes: SetArray['_indexes']; +} + +/** + * Typescript doesn't allow friend access to private fields, so this just casts the set into a type + * with public access modifiers. + */ +function cast(set: SetArray): PublicSet { + return set as any; +} + +/** + * Gets the index associated with `key` in the backing array, if it is already present. + */ +export function get(setarr: SetArray, key: T): number | undefined { + return cast(setarr)._indexes[key]; +} + +/** + * Puts `key` into the backing array, if it is not already present. Returns + * the index of the `key` in the backing array. + */ +export function put(setarr: SetArray, key: T): number { + // The key may or may not be present. If it is present, it's a number. + const index = get(setarr, key); + if (index !== undefined) return index; + + const { array, _indexes: indexes } = cast(setarr); + + const length = array.push(key); + return (indexes[key] = length - 1); +} + +/** + * Pops the last added item out of the SetArray. + */ +export function pop(setarr: SetArray): void { + const { array, _indexes: indexes } = cast(setarr); + if (array.length === 0) return; + + const last = array.pop()!; + indexes[last] = undefined; +} + +/** + * Removes the key, if it exists in the set. + */ +export function remove(setarr: SetArray, key: T): void { + const index = get(setarr, key); + if (index === undefined) return; + + const { array, _indexes: indexes } = cast(setarr); + for (let i = index + 1; i < array.length; i++) { + const k = array[i]; + array[i - 1] = k; + indexes[k]!--; + } + indexes[key] = undefined; + array.pop(); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/sourcemap-segment.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/sourcemap-segment.ts new file mode 100644 index 00000000..fb296dd3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/sourcemap-segment.ts @@ -0,0 +1,16 @@ +type GeneratedColumn = number; +type SourcesIndex = number; +type SourceLine = number; +type SourceColumn = number; +type NamesIndex = number; + +export type SourceMapSegment = + | [GeneratedColumn] + | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] + | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; + +export const COLUMN = 0; +export const SOURCES_INDEX = 1; +export const SOURCE_LINE = 2; +export const SOURCE_COLUMN = 3; +export const NAMES_INDEX = 4; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/types.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/types.ts new file mode 100644 index 00000000..b087f706 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/types.ts @@ -0,0 +1,61 @@ +// import type { GeneratedRange, OriginalScope } from '@jridgewell/sourcemap-codec'; +import type { SourceMapSegment } from './sourcemap-segment'; + +export interface SourceMapV3 { + file?: string | null; + names: readonly string[]; + sourceRoot?: string; + sources: readonly (string | null)[]; + sourcesContent?: readonly (string | null)[]; + version: 3; + ignoreList?: readonly number[]; +} + +export interface EncodedSourceMap extends SourceMapV3 { + mappings: string; + // originalScopes: string[]; + // generatedRanges: string; +} + +export interface DecodedSourceMap extends SourceMapV3 { + mappings: readonly SourceMapSegment[][]; + // originalScopes: readonly OriginalScope[][]; + // generatedRanges: readonly GeneratedRange[]; +} + +export interface Pos { + line: number; // 1-based + column: number; // 0-based +} + +export interface OriginalPos extends Pos { + source: string; +} + +export interface BindingExpressionRange { + start: Pos; + expression: string; +} + +// export type OriginalScopeInfo = [number, number, string[] | undefined]; +// export type GeneratedRangeInfo = [GeneratedRange, string[] | undefined]; + +export type Mapping = + | { + generated: Pos; + source: undefined; + original: undefined; + name: undefined; + } + | { + generated: Pos; + source: string; + original: Pos; + name: string; + } + | { + generated: Pos; + source: string; + original: Pos; + name: undefined; + }; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts new file mode 100644 index 00000000..7618d857 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts @@ -0,0 +1,89 @@ +import type { SourceMapInput } from '@jridgewell/trace-mapping'; +import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types.cts'; +export type { DecodedSourceMap, EncodedSourceMap, Mapping }; +export type Options = { + file?: string | null; + sourceRoot?: string | null; +}; +/** + * Provides the state to generate a sourcemap. + */ +export declare class GenMapping { + private _names; + private _sources; + private _sourcesContent; + private _mappings; + private _ignoreList; + file: string | null | undefined; + sourceRoot: string | null | undefined; + constructor({ file, sourceRoot }?: Options); +} +/** + * A low-level API to associate a generated position with an original source position. Line and + * column here are 0-based, unlike `addMapping`. + */ +export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source?: null, sourceLine?: null, sourceColumn?: null, name?: null, content?: null): void; +export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name?: null, content?: string | null): void; +export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name: string, content?: string | null): void; +/** + * A high-level API to associate a generated position with an original source position. Line is + * 1-based, but column is 0-based, due to legacy behavior in `source-map` library. + */ +export declare function addMapping(map: GenMapping, mapping: { + generated: Pos; + source?: null; + original?: null; + name?: null; + content?: null; +}): void; +export declare function addMapping(map: GenMapping, mapping: { + generated: Pos; + source: string; + original: Pos; + name?: null; + content?: string | null; +}): void; +export declare function addMapping(map: GenMapping, mapping: { + generated: Pos; + source: string; + original: Pos; + name: string; + content?: string | null; +}): void; +/** + * Same as `addSegment`, but will only add the segment if it generates useful information in the + * resulting map. This only works correctly if segments are added **in order**, meaning you should + * not add a segment with a lower generated line/column than one that came before. + */ +export declare const maybeAddSegment: typeof addSegment; +/** + * Same as `addMapping`, but will only add the mapping if it generates useful information in the + * resulting map. This only works correctly if mappings are added **in order**, meaning you should + * not add a mapping with a lower generated line/column than one that came before. + */ +export declare const maybeAddMapping: typeof addMapping; +/** + * Adds/removes the content of the source file to the source map. + */ +export declare function setSourceContent(map: GenMapping, source: string, content: string | null): void; +export declare function setIgnore(map: GenMapping, source: string, ignore?: boolean): void; +/** + * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function toDecodedMap(map: GenMapping): DecodedSourceMap; +/** + * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function toEncodedMap(map: GenMapping): EncodedSourceMap; +/** + * Constructs a new GenMapping, using the already present mappings of the input. + */ +export declare function fromMap(input: SourceMapInput): GenMapping; +/** + * Returns an array of high-level mapping objects for every recorded segment, which could then be + * passed to the `source-map` library. + */ +export declare function allMappings(map: GenMapping): Mapping[]; +//# sourceMappingURL=gen-mapping.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts.map new file mode 100644 index 00000000..8a2b1835 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"gen-mapping.d.ts","sourceRoot":"","sources":["../src/gen-mapping.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,GAAG,EACH,OAAO,EAKR,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAIF;;GAEG;AACH,qBAAa,UAAU;IACrB,QAAgB,MAAM,CAAmB;IACzC,QAAgB,QAAQ,CAAmB;IAC3C,QAAgB,eAAe,CAAoB;IACnD,QAAgB,SAAS,CAAuB;IAGhD,QAAgB,WAAW,CAAmB;IACtC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;gBAElC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAE,OAAY;CAW/C;AAoBD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,IAAI,EACb,UAAU,CAAC,EAAE,IAAI,EACjB,YAAY,CAAC,EAAE,IAAI,EACnB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,IAAI,GACb,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AAwBR;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AAcR;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAqBpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAEpC,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAS9F;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAO,QAYvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAwB9D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAO9D;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,UAAU,CAYzD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,EAAE,CA0BtD"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts new file mode 100644 index 00000000..bbc0d89c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts @@ -0,0 +1,89 @@ +import type { SourceMapInput } from '@jridgewell/trace-mapping'; +import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types.mts'; +export type { DecodedSourceMap, EncodedSourceMap, Mapping }; +export type Options = { + file?: string | null; + sourceRoot?: string | null; +}; +/** + * Provides the state to generate a sourcemap. + */ +export declare class GenMapping { + private _names; + private _sources; + private _sourcesContent; + private _mappings; + private _ignoreList; + file: string | null | undefined; + sourceRoot: string | null | undefined; + constructor({ file, sourceRoot }?: Options); +} +/** + * A low-level API to associate a generated position with an original source position. Line and + * column here are 0-based, unlike `addMapping`. + */ +export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source?: null, sourceLine?: null, sourceColumn?: null, name?: null, content?: null): void; +export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name?: null, content?: string | null): void; +export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name: string, content?: string | null): void; +/** + * A high-level API to associate a generated position with an original source position. Line is + * 1-based, but column is 0-based, due to legacy behavior in `source-map` library. + */ +export declare function addMapping(map: GenMapping, mapping: { + generated: Pos; + source?: null; + original?: null; + name?: null; + content?: null; +}): void; +export declare function addMapping(map: GenMapping, mapping: { + generated: Pos; + source: string; + original: Pos; + name?: null; + content?: string | null; +}): void; +export declare function addMapping(map: GenMapping, mapping: { + generated: Pos; + source: string; + original: Pos; + name: string; + content?: string | null; +}): void; +/** + * Same as `addSegment`, but will only add the segment if it generates useful information in the + * resulting map. This only works correctly if segments are added **in order**, meaning you should + * not add a segment with a lower generated line/column than one that came before. + */ +export declare const maybeAddSegment: typeof addSegment; +/** + * Same as `addMapping`, but will only add the mapping if it generates useful information in the + * resulting map. This only works correctly if mappings are added **in order**, meaning you should + * not add a mapping with a lower generated line/column than one that came before. + */ +export declare const maybeAddMapping: typeof addMapping; +/** + * Adds/removes the content of the source file to the source map. + */ +export declare function setSourceContent(map: GenMapping, source: string, content: string | null): void; +export declare function setIgnore(map: GenMapping, source: string, ignore?: boolean): void; +/** + * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function toDecodedMap(map: GenMapping): DecodedSourceMap; +/** + * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function toEncodedMap(map: GenMapping): EncodedSourceMap; +/** + * Constructs a new GenMapping, using the already present mappings of the input. + */ +export declare function fromMap(input: SourceMapInput): GenMapping; +/** + * Returns an array of high-level mapping objects for every recorded segment, which could then be + * passed to the `source-map` library. + */ +export declare function allMappings(map: GenMapping): Mapping[]; +//# sourceMappingURL=gen-mapping.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts.map new file mode 100644 index 00000000..8a2b1835 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"gen-mapping.d.ts","sourceRoot":"","sources":["../src/gen-mapping.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,GAAG,EACH,OAAO,EAKR,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAIF;;GAEG;AACH,qBAAa,UAAU;IACrB,QAAgB,MAAM,CAAmB;IACzC,QAAgB,QAAQ,CAAmB;IAC3C,QAAgB,eAAe,CAAoB;IACnD,QAAgB,SAAS,CAAuB;IAGhD,QAAgB,WAAW,CAAmB;IACtC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;gBAElC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAE,OAAY;CAW/C;AAoBD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,IAAI,EACb,UAAU,CAAC,EAAE,IAAI,EACjB,YAAY,CAAC,EAAE,IAAI,EACnB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,IAAI,GACb,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AAwBR;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AAcR;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAqBpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAEpC,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAS9F;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAO,QAYvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAwB9D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAO9D;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,UAAU,CAYzD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,EAAE,CA0BtD"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts new file mode 100644 index 00000000..5d8cda35 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts @@ -0,0 +1,33 @@ +type Key = string | number | symbol; +/** + * SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the + * index of the `key` in the backing array. + * + * This is designed to allow synchronizing a second array with the contents of the backing array, + * like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`, + * and there are never duplicates. + */ +export declare class SetArray { + private _indexes; + array: readonly T[]; + constructor(); +} +/** + * Gets the index associated with `key` in the backing array, if it is already present. + */ +export declare function get(setarr: SetArray, key: T): number | undefined; +/** + * Puts `key` into the backing array, if it is not already present. Returns + * the index of the `key` in the backing array. + */ +export declare function put(setarr: SetArray, key: T): number; +/** + * Pops the last added item out of the SetArray. + */ +export declare function pop(setarr: SetArray): void; +/** + * Removes the key, if it exists in the set. + */ +export declare function remove(setarr: SetArray, key: T): void; +export {}; +//# sourceMappingURL=set-array.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts.map new file mode 100644 index 00000000..c52b8bce --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"set-array.d.ts","sourceRoot":"","sources":["../src/set-array.ts"],"names":[],"mappings":"AAAA,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACvC,QAAgB,QAAQ,CAAgC;IAChD,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;;CAM7B;AAeD;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAYvE"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts new file mode 100644 index 00000000..5d8cda35 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts @@ -0,0 +1,33 @@ +type Key = string | number | symbol; +/** + * SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the + * index of the `key` in the backing array. + * + * This is designed to allow synchronizing a second array with the contents of the backing array, + * like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`, + * and there are never duplicates. + */ +export declare class SetArray { + private _indexes; + array: readonly T[]; + constructor(); +} +/** + * Gets the index associated with `key` in the backing array, if it is already present. + */ +export declare function get(setarr: SetArray, key: T): number | undefined; +/** + * Puts `key` into the backing array, if it is not already present. Returns + * the index of the `key` in the backing array. + */ +export declare function put(setarr: SetArray, key: T): number; +/** + * Pops the last added item out of the SetArray. + */ +export declare function pop(setarr: SetArray): void; +/** + * Removes the key, if it exists in the set. + */ +export declare function remove(setarr: SetArray, key: T): void; +export {}; +//# sourceMappingURL=set-array.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts.map new file mode 100644 index 00000000..c52b8bce --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"set-array.d.ts","sourceRoot":"","sources":["../src/set-array.ts"],"names":[],"mappings":"AAAA,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACvC,QAAgB,QAAQ,CAAgC;IAChD,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;;CAM7B;AAeD;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAYvE"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts new file mode 100644 index 00000000..68862952 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts @@ -0,0 +1,13 @@ +type GeneratedColumn = number; +type SourcesIndex = number; +type SourceLine = number; +type SourceColumn = number; +type NamesIndex = number; +export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; +export declare const COLUMN = 0; +export declare const SOURCES_INDEX = 1; +export declare const SOURCE_LINE = 2; +export declare const SOURCE_COLUMN = 3; +export declare const NAMES_INDEX = 4; +export {}; +//# sourceMappingURL=sourcemap-segment.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts.map new file mode 100644 index 00000000..23cdc452 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts new file mode 100644 index 00000000..68862952 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts @@ -0,0 +1,13 @@ +type GeneratedColumn = number; +type SourcesIndex = number; +type SourceLine = number; +type SourceColumn = number; +type NamesIndex = number; +export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; +export declare const COLUMN = 0; +export declare const SOURCES_INDEX = 1; +export declare const SOURCE_LINE = 2; +export declare const SOURCE_COLUMN = 3; +export declare const NAMES_INDEX = 4; +export {}; +//# sourceMappingURL=sourcemap-segment.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts.map new file mode 100644 index 00000000..23cdc452 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts new file mode 100644 index 00000000..58da00a9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts @@ -0,0 +1,44 @@ +import type { SourceMapSegment } from './sourcemap-segment.cts'; +export interface SourceMapV3 { + file?: string | null; + names: readonly string[]; + sourceRoot?: string; + sources: readonly (string | null)[]; + sourcesContent?: readonly (string | null)[]; + version: 3; + ignoreList?: readonly number[]; +} +export interface EncodedSourceMap extends SourceMapV3 { + mappings: string; +} +export interface DecodedSourceMap extends SourceMapV3 { + mappings: readonly SourceMapSegment[][]; +} +export interface Pos { + line: number; + column: number; +} +export interface OriginalPos extends Pos { + source: string; +} +export interface BindingExpressionRange { + start: Pos; + expression: string; +} +export type Mapping = { + generated: Pos; + source: undefined; + original: undefined; + name: undefined; +} | { + generated: Pos; + source: string; + original: Pos; + name: string; +} | { + generated: Pos; + source: string; + original: Pos; + name: undefined; +}; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts.map new file mode 100644 index 00000000..159e734d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAGlB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC;CAGzC;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,MAAM,MAAM,OAAO,GACf;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts new file mode 100644 index 00000000..e9837ebe --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts @@ -0,0 +1,44 @@ +import type { SourceMapSegment } from './sourcemap-segment.mts'; +export interface SourceMapV3 { + file?: string | null; + names: readonly string[]; + sourceRoot?: string; + sources: readonly (string | null)[]; + sourcesContent?: readonly (string | null)[]; + version: 3; + ignoreList?: readonly number[]; +} +export interface EncodedSourceMap extends SourceMapV3 { + mappings: string; +} +export interface DecodedSourceMap extends SourceMapV3 { + mappings: readonly SourceMapSegment[][]; +} +export interface Pos { + line: number; + column: number; +} +export interface OriginalPos extends Pos { + source: string; +} +export interface BindingExpressionRange { + start: Pos; + expression: string; +} +export type Mapping = { + generated: Pos; + source: undefined; + original: undefined; + name: undefined; +} | { + generated: Pos; + source: string; + original: Pos; + name: string; +} | { + generated: Pos; + source: string; + original: Pos; + name: undefined; +}; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts.map new file mode 100644 index 00000000..159e734d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAGlB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC;CAGzC;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,MAAM,MAAM,OAAO,GACf;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/LICENSE new file mode 100644 index 00000000..0a81b2ad --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/LICENSE @@ -0,0 +1,19 @@ +Copyright 2019 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/README.md new file mode 100644 index 00000000..2fe70df7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/README.md @@ -0,0 +1,40 @@ +# @jridgewell/resolve-uri + +> Resolve a URI relative to an optional base URI + +Resolve any combination of absolute URIs, protocol-realtive URIs, absolute paths, or relative paths. + +## Installation + +```sh +npm install @jridgewell/resolve-uri +``` + +## Usage + +```typescript +function resolve(input: string, base?: string): string; +``` + +```js +import resolve from '@jridgewell/resolve-uri'; + +resolve('foo', 'https://example.com'); // => 'https://example.com/foo' +``` + +| Input | Base | Resolution | Explanation | +|-----------------------|-------------------------|--------------------------------|--------------------------------------------------------------| +| `https://example.com` | _any_ | `https://example.com/` | Input is normalized only | +| `//example.com` | `https://base.com/` | `https://example.com/` | Input inherits the base's protocol | +| `//example.com` | _rest_ | `//example.com/` | Input is normalized only | +| `/example` | `https://base.com/` | `https://base.com/example` | Input inherits the base's origin | +| `/example` | `//base.com/` | `//base.com/example` | Input inherits the base's host and remains protocol relative | +| `/example` | _rest_ | `/example` | Input is normalized only | +| `example` | `https://base.com/dir/` | `https://base.com/dir/example` | Input is joined with the base | +| `example` | `https://base.com/file` | `https://base.com/example` | Input is joined with the base without its file | +| `example` | `//base.com/dir/` | `//base.com/dir/example` | Input is joined with the base's last directory | +| `example` | `//base.com/file` | `//base.com/example` | Input is joined with the base without its file | +| `example` | `/base/dir/` | `/base/dir/example` | Input is joined with the base's last directory | +| `example` | `/base/file` | `/base/example` | Input is joined with the base without its file | +| `example` | `base/dir/` | `base/dir/example` | Input is joined with the base's last directory | +| `example` | `base/file` | `base/example` | Input is joined with the base without its file | diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/package.json new file mode 100644 index 00000000..02a4c518 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/package.json @@ -0,0 +1,69 @@ +{ + "name": "@jridgewell/resolve-uri", + "version": "3.1.2", + "description": "Resolve a URI relative to an optional base URI", + "keywords": [ + "resolve", + "uri", + "url", + "path" + ], + "author": "Justin Ridgewell ", + "license": "MIT", + "repository": "https://github.com/jridgewell/resolve-uri", + "main": "dist/resolve-uri.umd.js", + "module": "dist/resolve-uri.mjs", + "types": "dist/types/resolve-uri.d.ts", + "exports": { + ".": [ + { + "types": "./dist/types/resolve-uri.d.ts", + "browser": "./dist/resolve-uri.umd.js", + "require": "./dist/resolve-uri.umd.js", + "import": "./dist/resolve-uri.mjs" + }, + "./dist/resolve-uri.umd.js" + ], + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=6.0.0" + }, + "scripts": { + "prebuild": "rm -rf dist", + "build": "run-s -n build:*", + "build:rollup": "rollup -c rollup.config.js", + "build:ts": "tsc --project tsconfig.build.json", + "lint": "run-s -n lint:*", + "lint:prettier": "npm run test:lint:prettier -- --write", + "lint:ts": "npm run test:lint:ts -- --fix", + "pretest": "run-s build:rollup", + "test": "run-s -n test:lint test:only", + "test:debug": "mocha --inspect-brk", + "test:lint": "run-s -n test:lint:*", + "test:lint:prettier": "prettier --check '{src,test}/**/*.ts'", + "test:lint:ts": "eslint '{src,test}/**/*.ts'", + "test:only": "mocha", + "test:coverage": "c8 mocha", + "test:watch": "mocha --watch", + "prepublishOnly": "npm run preversion", + "preversion": "run-s test build" + }, + "devDependencies": { + "@jridgewell/resolve-uri-latest": "npm:@jridgewell/resolve-uri@*", + "@rollup/plugin-typescript": "8.3.0", + "@typescript-eslint/eslint-plugin": "5.10.0", + "@typescript-eslint/parser": "5.10.0", + "c8": "7.11.0", + "eslint": "8.7.0", + "eslint-config-prettier": "8.3.0", + "mocha": "9.2.0", + "npm-run-all": "4.1.5", + "prettier": "2.5.1", + "rollup": "2.66.0", + "typescript": "4.5.5" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/LICENSE new file mode 100644 index 00000000..1f6ce94c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/LICENSE @@ -0,0 +1,19 @@ +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/README.md new file mode 100644 index 00000000..b3e0708b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/README.md @@ -0,0 +1,264 @@ +# @jridgewell/sourcemap-codec + +Encode/decode the `mappings` property of a [sourcemap](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit). + + +## Why? + +Sourcemaps are difficult to generate and manipulate, because the `mappings` property – the part that actually links the generated code back to the original source – is encoded using an obscure method called [Variable-length quantity](https://en.wikipedia.org/wiki/Variable-length_quantity). On top of that, each segment in the mapping contains offsets rather than absolute indices, which means that you can't look at a segment in isolation – you have to understand the whole sourcemap. + +This package makes the process slightly easier. + + +## Installation + +```bash +npm install @jridgewell/sourcemap-codec +``` + + +## Usage + +```js +import { encode, decode } from '@jridgewell/sourcemap-codec'; + +var decoded = decode( ';EAEEA,EAAE,EAAC,CAAE;ECQY,UACC' ); + +assert.deepEqual( decoded, [ + // the first line (of the generated code) has no mappings, + // as shown by the starting semi-colon (which separates lines) + [], + + // the second line contains four (comma-separated) segments + [ + // segments are encoded as you'd expect: + // [ generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex ] + + // i.e. the first segment begins at column 2, and maps back to the second column + // of the second line (both zero-based) of the 0th source, and uses the 0th + // name in the `map.names` array + [ 2, 0, 2, 2, 0 ], + + // the remaining segments are 4-length rather than 5-length, + // because they don't map a name + [ 4, 0, 2, 4 ], + [ 6, 0, 2, 5 ], + [ 7, 0, 2, 7 ] + ], + + // the final line contains two segments + [ + [ 2, 1, 10, 19 ], + [ 12, 1, 11, 20 ] + ] +]); + +var encoded = encode( decoded ); +assert.equal( encoded, ';EAEEA,EAAE,EAAC,CAAE;ECQY,UACC' ); +``` + +## Benchmarks + +``` +node v20.10.0 + +amp.js.map - 45120 segments + +Decode Memory Usage: +local code 5815135 bytes +@jridgewell/sourcemap-codec 1.4.15 5868160 bytes +sourcemap-codec 5492584 bytes +source-map-0.6.1 13569984 bytes +source-map-0.8.0 6390584 bytes +chrome dev tools 8011136 bytes +Smallest memory usage is sourcemap-codec + +Decode speed: +decode: local code x 492 ops/sec ±1.22% (90 runs sampled) +decode: @jridgewell/sourcemap-codec 1.4.15 x 499 ops/sec ±1.16% (89 runs sampled) +decode: sourcemap-codec x 376 ops/sec ±1.66% (89 runs sampled) +decode: source-map-0.6.1 x 34.99 ops/sec ±0.94% (48 runs sampled) +decode: source-map-0.8.0 x 351 ops/sec ±0.07% (95 runs sampled) +chrome dev tools x 165 ops/sec ±0.91% (86 runs sampled) +Fastest is decode: @jridgewell/sourcemap-codec 1.4.15 + +Encode Memory Usage: +local code 444248 bytes +@jridgewell/sourcemap-codec 1.4.15 623024 bytes +sourcemap-codec 8696280 bytes +source-map-0.6.1 8745176 bytes +source-map-0.8.0 8736624 bytes +Smallest memory usage is local code + +Encode speed: +encode: local code x 796 ops/sec ±0.11% (97 runs sampled) +encode: @jridgewell/sourcemap-codec 1.4.15 x 795 ops/sec ±0.25% (98 runs sampled) +encode: sourcemap-codec x 231 ops/sec ±0.83% (86 runs sampled) +encode: source-map-0.6.1 x 166 ops/sec ±0.57% (86 runs sampled) +encode: source-map-0.8.0 x 203 ops/sec ±0.45% (88 runs sampled) +Fastest is encode: local code,encode: @jridgewell/sourcemap-codec 1.4.15 + + +*** + + +babel.min.js.map - 347793 segments + +Decode Memory Usage: +local code 35424960 bytes +@jridgewell/sourcemap-codec 1.4.15 35424696 bytes +sourcemap-codec 36033464 bytes +source-map-0.6.1 62253704 bytes +source-map-0.8.0 43843920 bytes +chrome dev tools 45111400 bytes +Smallest memory usage is @jridgewell/sourcemap-codec 1.4.15 + +Decode speed: +decode: local code x 38.18 ops/sec ±5.44% (52 runs sampled) +decode: @jridgewell/sourcemap-codec 1.4.15 x 38.36 ops/sec ±5.02% (52 runs sampled) +decode: sourcemap-codec x 34.05 ops/sec ±4.45% (47 runs sampled) +decode: source-map-0.6.1 x 4.31 ops/sec ±2.76% (15 runs sampled) +decode: source-map-0.8.0 x 55.60 ops/sec ±0.13% (73 runs sampled) +chrome dev tools x 16.94 ops/sec ±3.78% (46 runs sampled) +Fastest is decode: source-map-0.8.0 + +Encode Memory Usage: +local code 2606016 bytes +@jridgewell/sourcemap-codec 1.4.15 2626440 bytes +sourcemap-codec 21152576 bytes +source-map-0.6.1 25023928 bytes +source-map-0.8.0 25256448 bytes +Smallest memory usage is local code + +Encode speed: +encode: local code x 127 ops/sec ±0.18% (83 runs sampled) +encode: @jridgewell/sourcemap-codec 1.4.15 x 128 ops/sec ±0.26% (83 runs sampled) +encode: sourcemap-codec x 29.31 ops/sec ±2.55% (53 runs sampled) +encode: source-map-0.6.1 x 18.85 ops/sec ±3.19% (36 runs sampled) +encode: source-map-0.8.0 x 19.34 ops/sec ±1.97% (36 runs sampled) +Fastest is encode: @jridgewell/sourcemap-codec 1.4.15 + + +*** + + +preact.js.map - 1992 segments + +Decode Memory Usage: +local code 261696 bytes +@jridgewell/sourcemap-codec 1.4.15 244296 bytes +sourcemap-codec 302816 bytes +source-map-0.6.1 939176 bytes +source-map-0.8.0 336 bytes +chrome dev tools 587368 bytes +Smallest memory usage is source-map-0.8.0 + +Decode speed: +decode: local code x 17,782 ops/sec ±0.32% (97 runs sampled) +decode: @jridgewell/sourcemap-codec 1.4.15 x 17,863 ops/sec ±0.40% (100 runs sampled) +decode: sourcemap-codec x 12,453 ops/sec ±0.27% (101 runs sampled) +decode: source-map-0.6.1 x 1,288 ops/sec ±1.05% (96 runs sampled) +decode: source-map-0.8.0 x 9,289 ops/sec ±0.27% (101 runs sampled) +chrome dev tools x 4,769 ops/sec ±0.18% (100 runs sampled) +Fastest is decode: @jridgewell/sourcemap-codec 1.4.15 + +Encode Memory Usage: +local code 262944 bytes +@jridgewell/sourcemap-codec 1.4.15 25544 bytes +sourcemap-codec 323048 bytes +source-map-0.6.1 507808 bytes +source-map-0.8.0 507480 bytes +Smallest memory usage is @jridgewell/sourcemap-codec 1.4.15 + +Encode speed: +encode: local code x 24,207 ops/sec ±0.79% (95 runs sampled) +encode: @jridgewell/sourcemap-codec 1.4.15 x 24,288 ops/sec ±0.48% (96 runs sampled) +encode: sourcemap-codec x 6,761 ops/sec ±0.21% (100 runs sampled) +encode: source-map-0.6.1 x 5,374 ops/sec ±0.17% (99 runs sampled) +encode: source-map-0.8.0 x 5,633 ops/sec ±0.32% (99 runs sampled) +Fastest is encode: @jridgewell/sourcemap-codec 1.4.15,encode: local code + + +*** + + +react.js.map - 5726 segments + +Decode Memory Usage: +local code 678816 bytes +@jridgewell/sourcemap-codec 1.4.15 678816 bytes +sourcemap-codec 816400 bytes +source-map-0.6.1 2288864 bytes +source-map-0.8.0 721360 bytes +chrome dev tools 1012512 bytes +Smallest memory usage is local code + +Decode speed: +decode: local code x 6,178 ops/sec ±0.19% (98 runs sampled) +decode: @jridgewell/sourcemap-codec 1.4.15 x 6,261 ops/sec ±0.22% (100 runs sampled) +decode: sourcemap-codec x 4,472 ops/sec ±0.90% (99 runs sampled) +decode: source-map-0.6.1 x 449 ops/sec ±0.31% (95 runs sampled) +decode: source-map-0.8.0 x 3,219 ops/sec ±0.13% (100 runs sampled) +chrome dev tools x 1,743 ops/sec ±0.20% (99 runs sampled) +Fastest is decode: @jridgewell/sourcemap-codec 1.4.15 + +Encode Memory Usage: +local code 140960 bytes +@jridgewell/sourcemap-codec 1.4.15 159808 bytes +sourcemap-codec 969304 bytes +source-map-0.6.1 930520 bytes +source-map-0.8.0 930248 bytes +Smallest memory usage is local code + +Encode speed: +encode: local code x 8,013 ops/sec ±0.19% (100 runs sampled) +encode: @jridgewell/sourcemap-codec 1.4.15 x 7,989 ops/sec ±0.20% (101 runs sampled) +encode: sourcemap-codec x 2,472 ops/sec ±0.21% (99 runs sampled) +encode: source-map-0.6.1 x 2,200 ops/sec ±0.17% (99 runs sampled) +encode: source-map-0.8.0 x 2,220 ops/sec ±0.37% (99 runs sampled) +Fastest is encode: local code + + +*** + + +vscode.map - 2141001 segments + +Decode Memory Usage: +local code 198955264 bytes +@jridgewell/sourcemap-codec 1.4.15 199175352 bytes +sourcemap-codec 199102688 bytes +source-map-0.6.1 386323432 bytes +source-map-0.8.0 244116432 bytes +chrome dev tools 293734280 bytes +Smallest memory usage is local code + +Decode speed: +decode: local code x 3.90 ops/sec ±22.21% (15 runs sampled) +decode: @jridgewell/sourcemap-codec 1.4.15 x 3.95 ops/sec ±23.53% (15 runs sampled) +decode: sourcemap-codec x 3.82 ops/sec ±17.94% (14 runs sampled) +decode: source-map-0.6.1 x 0.61 ops/sec ±7.81% (6 runs sampled) +decode: source-map-0.8.0 x 9.54 ops/sec ±0.28% (28 runs sampled) +chrome dev tools x 2.18 ops/sec ±10.58% (10 runs sampled) +Fastest is decode: source-map-0.8.0 + +Encode Memory Usage: +local code 13509880 bytes +@jridgewell/sourcemap-codec 1.4.15 13537648 bytes +sourcemap-codec 32540104 bytes +source-map-0.6.1 127531040 bytes +source-map-0.8.0 127535312 bytes +Smallest memory usage is local code + +Encode speed: +encode: local code x 20.10 ops/sec ±0.19% (38 runs sampled) +encode: @jridgewell/sourcemap-codec 1.4.15 x 20.26 ops/sec ±0.32% (38 runs sampled) +encode: sourcemap-codec x 5.44 ops/sec ±1.64% (18 runs sampled) +encode: source-map-0.6.1 x 2.30 ops/sec ±4.79% (10 runs sampled) +encode: source-map-0.8.0 x 2.46 ops/sec ±6.53% (10 runs sampled) +Fastest is encode: @jridgewell/sourcemap-codec 1.4.15 +``` + +# License + +MIT diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/package.json new file mode 100644 index 00000000..da551376 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/package.json @@ -0,0 +1,63 @@ +{ + "name": "@jridgewell/sourcemap-codec", + "version": "1.5.5", + "description": "Encode/decode sourcemap mappings", + "keywords": [ + "sourcemap", + "vlq" + ], + "main": "dist/sourcemap-codec.umd.js", + "module": "dist/sourcemap-codec.mjs", + "types": "types/sourcemap-codec.d.cts", + "files": [ + "dist", + "src", + "types" + ], + "exports": { + ".": [ + { + "import": { + "types": "./types/sourcemap-codec.d.mts", + "default": "./dist/sourcemap-codec.mjs" + }, + "default": { + "types": "./types/sourcemap-codec.d.cts", + "default": "./dist/sourcemap-codec.umd.js" + } + }, + "./dist/sourcemap-codec.umd.js" + ], + "./package.json": "./package.json" + }, + "scripts": { + "benchmark": "run-s build:code benchmark:*", + "benchmark:install": "cd benchmark && npm install", + "benchmark:only": "node --expose-gc benchmark/index.js", + "build": "run-s -n build:code build:types", + "build:code": "node ../../esbuild.mjs sourcemap-codec.ts", + "build:types": "run-s build:types:force build:types:emit build:types:mts", + "build:types:force": "rimraf tsconfig.build.tsbuildinfo", + "build:types:emit": "tsc --project tsconfig.build.json", + "build:types:mts": "node ../../mts-types.mjs", + "clean": "run-s -n clean:code clean:types", + "clean:code": "tsc --build --clean tsconfig.build.json", + "clean:types": "rimraf dist types", + "test": "run-s -n test:types test:only test:format", + "test:format": "prettier --check '{src,test}/**/*.ts'", + "test:only": "mocha", + "test:types": "eslint '{src,test}/**/*.ts'", + "lint": "run-s -n lint:types lint:format", + "lint:format": "npm run test:format -- --write", + "lint:types": "npm run test:types -- --fix", + "prepublishOnly": "npm run-s -n build test" + }, + "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec", + "repository": { + "type": "git", + "url": "git+https://github.com/jridgewell/sourcemaps.git", + "directory": "packages/sourcemap-codec" + }, + "author": "Justin Ridgewell ", + "license": "MIT" +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/scopes.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/scopes.ts new file mode 100644 index 00000000..d194c2f0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/scopes.ts @@ -0,0 +1,345 @@ +import { StringReader, StringWriter } from './strings'; +import { comma, decodeInteger, encodeInteger, hasMoreVlq, semicolon } from './vlq'; + +const EMPTY: any[] = []; + +type Line = number; +type Column = number; +type Kind = number; +type Name = number; +type Var = number; +type SourcesIndex = number; +type ScopesIndex = number; + +type Mix = (A & O) | (B & O); + +export type OriginalScope = Mix< + [Line, Column, Line, Column, Kind], + [Line, Column, Line, Column, Kind, Name], + { vars: Var[] } +>; + +export type GeneratedRange = Mix< + [Line, Column, Line, Column], + [Line, Column, Line, Column, SourcesIndex, ScopesIndex], + { + callsite: CallSite | null; + bindings: Binding[]; + isScope: boolean; + } +>; +export type CallSite = [SourcesIndex, Line, Column]; +type Binding = BindingExpressionRange[]; +export type BindingExpressionRange = [Name] | [Name, Line, Column]; + +export function decodeOriginalScopes(input: string): OriginalScope[] { + const { length } = input; + const reader = new StringReader(input); + const scopes: OriginalScope[] = []; + const stack: OriginalScope[] = []; + let line = 0; + + for (; reader.pos < length; reader.pos++) { + line = decodeInteger(reader, line); + const column = decodeInteger(reader, 0); + + if (!hasMoreVlq(reader, length)) { + const last = stack.pop()!; + last[2] = line; + last[3] = column; + continue; + } + + const kind = decodeInteger(reader, 0); + const fields = decodeInteger(reader, 0); + const hasName = fields & 0b0001; + + const scope: OriginalScope = ( + hasName ? [line, column, 0, 0, kind, decodeInteger(reader, 0)] : [line, column, 0, 0, kind] + ) as OriginalScope; + + let vars: Var[] = EMPTY; + if (hasMoreVlq(reader, length)) { + vars = []; + do { + const varsIndex = decodeInteger(reader, 0); + vars.push(varsIndex); + } while (hasMoreVlq(reader, length)); + } + scope.vars = vars; + + scopes.push(scope); + stack.push(scope); + } + + return scopes; +} + +export function encodeOriginalScopes(scopes: OriginalScope[]): string { + const writer = new StringWriter(); + + for (let i = 0; i < scopes.length; ) { + i = _encodeOriginalScopes(scopes, i, writer, [0]); + } + + return writer.flush(); +} + +function _encodeOriginalScopes( + scopes: OriginalScope[], + index: number, + writer: StringWriter, + state: [ + number, // GenColumn + ], +): number { + const scope = scopes[index]; + const { 0: startLine, 1: startColumn, 2: endLine, 3: endColumn, 4: kind, vars } = scope; + + if (index > 0) writer.write(comma); + + state[0] = encodeInteger(writer, startLine, state[0]); + encodeInteger(writer, startColumn, 0); + encodeInteger(writer, kind, 0); + + const fields = scope.length === 6 ? 0b0001 : 0; + encodeInteger(writer, fields, 0); + if (scope.length === 6) encodeInteger(writer, scope[5], 0); + + for (const v of vars) { + encodeInteger(writer, v, 0); + } + + for (index++; index < scopes.length; ) { + const next = scopes[index]; + const { 0: l, 1: c } = next; + if (l > endLine || (l === endLine && c >= endColumn)) { + break; + } + index = _encodeOriginalScopes(scopes, index, writer, state); + } + + writer.write(comma); + state[0] = encodeInteger(writer, endLine, state[0]); + encodeInteger(writer, endColumn, 0); + + return index; +} + +export function decodeGeneratedRanges(input: string): GeneratedRange[] { + const { length } = input; + const reader = new StringReader(input); + const ranges: GeneratedRange[] = []; + const stack: GeneratedRange[] = []; + + let genLine = 0; + let definitionSourcesIndex = 0; + let definitionScopeIndex = 0; + let callsiteSourcesIndex = 0; + let callsiteLine = 0; + let callsiteColumn = 0; + let bindingLine = 0; + let bindingColumn = 0; + + do { + const semi = reader.indexOf(';'); + let genColumn = 0; + + for (; reader.pos < semi; reader.pos++) { + genColumn = decodeInteger(reader, genColumn); + + if (!hasMoreVlq(reader, semi)) { + const last = stack.pop()!; + last[2] = genLine; + last[3] = genColumn; + continue; + } + + const fields = decodeInteger(reader, 0); + const hasDefinition = fields & 0b0001; + const hasCallsite = fields & 0b0010; + const hasScope = fields & 0b0100; + + let callsite: CallSite | null = null; + let bindings: Binding[] = EMPTY; + let range: GeneratedRange; + if (hasDefinition) { + const defSourcesIndex = decodeInteger(reader, definitionSourcesIndex); + definitionScopeIndex = decodeInteger( + reader, + definitionSourcesIndex === defSourcesIndex ? definitionScopeIndex : 0, + ); + + definitionSourcesIndex = defSourcesIndex; + range = [genLine, genColumn, 0, 0, defSourcesIndex, definitionScopeIndex] as GeneratedRange; + } else { + range = [genLine, genColumn, 0, 0] as GeneratedRange; + } + + range.isScope = !!hasScope; + + if (hasCallsite) { + const prevCsi = callsiteSourcesIndex; + const prevLine = callsiteLine; + callsiteSourcesIndex = decodeInteger(reader, callsiteSourcesIndex); + const sameSource = prevCsi === callsiteSourcesIndex; + callsiteLine = decodeInteger(reader, sameSource ? callsiteLine : 0); + callsiteColumn = decodeInteger( + reader, + sameSource && prevLine === callsiteLine ? callsiteColumn : 0, + ); + + callsite = [callsiteSourcesIndex, callsiteLine, callsiteColumn]; + } + range.callsite = callsite; + + if (hasMoreVlq(reader, semi)) { + bindings = []; + do { + bindingLine = genLine; + bindingColumn = genColumn; + const expressionsCount = decodeInteger(reader, 0); + let expressionRanges: BindingExpressionRange[]; + if (expressionsCount < -1) { + expressionRanges = [[decodeInteger(reader, 0)]]; + for (let i = -1; i > expressionsCount; i--) { + const prevBl = bindingLine; + bindingLine = decodeInteger(reader, bindingLine); + bindingColumn = decodeInteger(reader, bindingLine === prevBl ? bindingColumn : 0); + const expression = decodeInteger(reader, 0); + expressionRanges.push([expression, bindingLine, bindingColumn]); + } + } else { + expressionRanges = [[expressionsCount]]; + } + bindings.push(expressionRanges); + } while (hasMoreVlq(reader, semi)); + } + range.bindings = bindings; + + ranges.push(range); + stack.push(range); + } + + genLine++; + reader.pos = semi + 1; + } while (reader.pos < length); + + return ranges; +} + +export function encodeGeneratedRanges(ranges: GeneratedRange[]): string { + if (ranges.length === 0) return ''; + + const writer = new StringWriter(); + + for (let i = 0; i < ranges.length; ) { + i = _encodeGeneratedRanges(ranges, i, writer, [0, 0, 0, 0, 0, 0, 0]); + } + + return writer.flush(); +} + +function _encodeGeneratedRanges( + ranges: GeneratedRange[], + index: number, + writer: StringWriter, + state: [ + number, // GenLine + number, // GenColumn + number, // DefSourcesIndex + number, // DefScopesIndex + number, // CallSourcesIndex + number, // CallLine + number, // CallColumn + ], +): number { + const range = ranges[index]; + const { + 0: startLine, + 1: startColumn, + 2: endLine, + 3: endColumn, + isScope, + callsite, + bindings, + } = range; + + if (state[0] < startLine) { + catchupLine(writer, state[0], startLine); + state[0] = startLine; + state[1] = 0; + } else if (index > 0) { + writer.write(comma); + } + + state[1] = encodeInteger(writer, range[1], state[1]); + + const fields = + (range.length === 6 ? 0b0001 : 0) | (callsite ? 0b0010 : 0) | (isScope ? 0b0100 : 0); + encodeInteger(writer, fields, 0); + + if (range.length === 6) { + const { 4: sourcesIndex, 5: scopesIndex } = range; + if (sourcesIndex !== state[2]) { + state[3] = 0; + } + state[2] = encodeInteger(writer, sourcesIndex, state[2]); + state[3] = encodeInteger(writer, scopesIndex, state[3]); + } + + if (callsite) { + const { 0: sourcesIndex, 1: callLine, 2: callColumn } = range.callsite!; + if (sourcesIndex !== state[4]) { + state[5] = 0; + state[6] = 0; + } else if (callLine !== state[5]) { + state[6] = 0; + } + state[4] = encodeInteger(writer, sourcesIndex, state[4]); + state[5] = encodeInteger(writer, callLine, state[5]); + state[6] = encodeInteger(writer, callColumn, state[6]); + } + + if (bindings) { + for (const binding of bindings) { + if (binding.length > 1) encodeInteger(writer, -binding.length, 0); + const expression = binding[0][0]; + encodeInteger(writer, expression, 0); + let bindingStartLine = startLine; + let bindingStartColumn = startColumn; + for (let i = 1; i < binding.length; i++) { + const expRange = binding[i]; + bindingStartLine = encodeInteger(writer, expRange[1]!, bindingStartLine); + bindingStartColumn = encodeInteger(writer, expRange[2]!, bindingStartColumn); + encodeInteger(writer, expRange[0]!, 0); + } + } + } + + for (index++; index < ranges.length; ) { + const next = ranges[index]; + const { 0: l, 1: c } = next; + if (l > endLine || (l === endLine && c >= endColumn)) { + break; + } + index = _encodeGeneratedRanges(ranges, index, writer, state); + } + + if (state[0] < endLine) { + catchupLine(writer, state[0], endLine); + state[0] = endLine; + state[1] = 0; + } else { + writer.write(comma); + } + state[1] = encodeInteger(writer, endColumn, state[1]); + + return index; +} + +function catchupLine(writer: StringWriter, lastLine: number, line: number) { + do { + writer.write(semicolon); + } while (++lastLine < line); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/sourcemap-codec.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/sourcemap-codec.ts new file mode 100644 index 00000000..a81f894d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/sourcemap-codec.ts @@ -0,0 +1,111 @@ +import { comma, decodeInteger, encodeInteger, hasMoreVlq, semicolon } from './vlq'; +import { StringWriter, StringReader } from './strings'; + +export { + decodeOriginalScopes, + encodeOriginalScopes, + decodeGeneratedRanges, + encodeGeneratedRanges, +} from './scopes'; +export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes'; + +export type SourceMapSegment = + | [number] + | [number, number, number, number] + | [number, number, number, number, number]; +export type SourceMapLine = SourceMapSegment[]; +export type SourceMapMappings = SourceMapLine[]; + +export function decode(mappings: string): SourceMapMappings { + const { length } = mappings; + const reader = new StringReader(mappings); + const decoded: SourceMapMappings = []; + let genColumn = 0; + let sourcesIndex = 0; + let sourceLine = 0; + let sourceColumn = 0; + let namesIndex = 0; + + do { + const semi = reader.indexOf(';'); + const line: SourceMapLine = []; + let sorted = true; + let lastCol = 0; + genColumn = 0; + + while (reader.pos < semi) { + let seg: SourceMapSegment; + + genColumn = decodeInteger(reader, genColumn); + if (genColumn < lastCol) sorted = false; + lastCol = genColumn; + + if (hasMoreVlq(reader, semi)) { + sourcesIndex = decodeInteger(reader, sourcesIndex); + sourceLine = decodeInteger(reader, sourceLine); + sourceColumn = decodeInteger(reader, sourceColumn); + + if (hasMoreVlq(reader, semi)) { + namesIndex = decodeInteger(reader, namesIndex); + seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]; + } else { + seg = [genColumn, sourcesIndex, sourceLine, sourceColumn]; + } + } else { + seg = [genColumn]; + } + + line.push(seg); + reader.pos++; + } + + if (!sorted) sort(line); + decoded.push(line); + reader.pos = semi + 1; + } while (reader.pos <= length); + + return decoded; +} + +function sort(line: SourceMapSegment[]) { + line.sort(sortComparator); +} + +function sortComparator(a: SourceMapSegment, b: SourceMapSegment): number { + return a[0] - b[0]; +} + +export function encode(decoded: SourceMapMappings): string; +export function encode(decoded: Readonly): string; +export function encode(decoded: Readonly): string { + const writer = new StringWriter(); + let sourcesIndex = 0; + let sourceLine = 0; + let sourceColumn = 0; + let namesIndex = 0; + + for (let i = 0; i < decoded.length; i++) { + const line = decoded[i]; + if (i > 0) writer.write(semicolon); + if (line.length === 0) continue; + + let genColumn = 0; + + for (let j = 0; j < line.length; j++) { + const segment = line[j]; + if (j > 0) writer.write(comma); + + genColumn = encodeInteger(writer, segment[0], genColumn); + + if (segment.length === 1) continue; + sourcesIndex = encodeInteger(writer, segment[1], sourcesIndex); + sourceLine = encodeInteger(writer, segment[2], sourceLine); + sourceColumn = encodeInteger(writer, segment[3], sourceColumn); + + if (segment.length === 4) continue; + namesIndex = encodeInteger(writer, segment[4], namesIndex); + } + } + + return writer.flush(); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/strings.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/strings.ts new file mode 100644 index 00000000..d1619650 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/strings.ts @@ -0,0 +1,65 @@ +const bufLength = 1024 * 16; + +// Provide a fallback for older environments. +const td = + typeof TextDecoder !== 'undefined' + ? /* #__PURE__ */ new TextDecoder() + : typeof Buffer !== 'undefined' + ? { + decode(buf: Uint8Array): string { + const out = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength); + return out.toString(); + }, + } + : { + decode(buf: Uint8Array): string { + let out = ''; + for (let i = 0; i < buf.length; i++) { + out += String.fromCharCode(buf[i]); + } + return out; + }, + }; + +export class StringWriter { + pos = 0; + private out = ''; + private buffer = new Uint8Array(bufLength); + + write(v: number): void { + const { buffer } = this; + buffer[this.pos++] = v; + if (this.pos === bufLength) { + this.out += td.decode(buffer); + this.pos = 0; + } + } + + flush(): string { + const { buffer, out, pos } = this; + return pos > 0 ? out + td.decode(buffer.subarray(0, pos)) : out; + } +} + +export class StringReader { + pos = 0; + declare private buffer: string; + + constructor(buffer: string) { + this.buffer = buffer; + } + + next(): number { + return this.buffer.charCodeAt(this.pos++); + } + + peek(): number { + return this.buffer.charCodeAt(this.pos); + } + + indexOf(char: string): number { + const { buffer, pos } = this; + const idx = buffer.indexOf(char, pos); + return idx === -1 ? buffer.length : idx; + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/vlq.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/vlq.ts new file mode 100644 index 00000000..a42c6815 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/vlq.ts @@ -0,0 +1,55 @@ +import type { StringReader, StringWriter } from './strings'; + +export const comma = ','.charCodeAt(0); +export const semicolon = ';'.charCodeAt(0); + +const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +const intToChar = new Uint8Array(64); // 64 possible chars. +const charToInt = new Uint8Array(128); // z is 122 in ASCII + +for (let i = 0; i < chars.length; i++) { + const c = chars.charCodeAt(i); + intToChar[i] = c; + charToInt[c] = i; +} + +export function decodeInteger(reader: StringReader, relative: number): number { + let value = 0; + let shift = 0; + let integer = 0; + + do { + const c = reader.next(); + integer = charToInt[c]; + value |= (integer & 31) << shift; + shift += 5; + } while (integer & 32); + + const shouldNegate = value & 1; + value >>>= 1; + + if (shouldNegate) { + value = -0x80000000 | -value; + } + + return relative + value; +} + +export function encodeInteger(builder: StringWriter, num: number, relative: number): number { + let delta = num - relative; + + delta = delta < 0 ? (-delta << 1) | 1 : delta << 1; + do { + let clamped = delta & 0b011111; + delta >>>= 5; + if (delta > 0) clamped |= 0b100000; + builder.write(intToChar[clamped]); + } while (delta > 0); + + return num; +} + +export function hasMoreVlq(reader: StringReader, max: number) { + if (reader.pos >= max) return false; + return reader.peek() !== comma; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts new file mode 100644 index 00000000..c583c756 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts @@ -0,0 +1,50 @@ +type Line = number; +type Column = number; +type Kind = number; +type Name = number; +type Var = number; +type SourcesIndex = number; +type ScopesIndex = number; +type Mix = (A & O) | (B & O); +export type OriginalScope = Mix<[ + Line, + Column, + Line, + Column, + Kind +], [ + Line, + Column, + Line, + Column, + Kind, + Name +], { + vars: Var[]; +}>; +export type GeneratedRange = Mix<[ + Line, + Column, + Line, + Column +], [ + Line, + Column, + Line, + Column, + SourcesIndex, + ScopesIndex +], { + callsite: CallSite | null; + bindings: Binding[]; + isScope: boolean; +}>; +export type CallSite = [SourcesIndex, Line, Column]; +type Binding = BindingExpressionRange[]; +export type BindingExpressionRange = [Name] | [Name, Line, Column]; +export declare function decodeOriginalScopes(input: string): OriginalScope[]; +export declare function encodeOriginalScopes(scopes: OriginalScope[]): string; +export declare function decodeGeneratedRanges(input: string): GeneratedRange[]; +export declare function encodeGeneratedRanges(ranges: GeneratedRange[]): string; +export {}; +//# sourceMappingURL=scopes.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts.map new file mode 100644 index 00000000..630e6477 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAKA,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,GAAG,GAAG,MAAM,CAAC;AAClB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,WAAW,GAAG,MAAM,CAAC;AAE1B,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,GAAG,CAC7B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;CAAC,EAClC;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,IAAI;CAAC,EACxC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,GAAG,CAC9B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;CAAC,EAC5B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,YAAY;IAAE,WAAW;CAAC,EACvD;IACE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB,CACF,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACpD,KAAK,OAAO,GAAG,sBAAsB,EAAE,CAAC;AACxC,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEnE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAyCnE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAQpE;AA2CD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAoGrE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAUtE"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts new file mode 100644 index 00000000..c583c756 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts @@ -0,0 +1,50 @@ +type Line = number; +type Column = number; +type Kind = number; +type Name = number; +type Var = number; +type SourcesIndex = number; +type ScopesIndex = number; +type Mix = (A & O) | (B & O); +export type OriginalScope = Mix<[ + Line, + Column, + Line, + Column, + Kind +], [ + Line, + Column, + Line, + Column, + Kind, + Name +], { + vars: Var[]; +}>; +export type GeneratedRange = Mix<[ + Line, + Column, + Line, + Column +], [ + Line, + Column, + Line, + Column, + SourcesIndex, + ScopesIndex +], { + callsite: CallSite | null; + bindings: Binding[]; + isScope: boolean; +}>; +export type CallSite = [SourcesIndex, Line, Column]; +type Binding = BindingExpressionRange[]; +export type BindingExpressionRange = [Name] | [Name, Line, Column]; +export declare function decodeOriginalScopes(input: string): OriginalScope[]; +export declare function encodeOriginalScopes(scopes: OriginalScope[]): string; +export declare function decodeGeneratedRanges(input: string): GeneratedRange[]; +export declare function encodeGeneratedRanges(ranges: GeneratedRange[]): string; +export {}; +//# sourceMappingURL=scopes.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts.map new file mode 100644 index 00000000..630e6477 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAKA,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,GAAG,GAAG,MAAM,CAAC;AAClB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,WAAW,GAAG,MAAM,CAAC;AAE1B,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,GAAG,CAC7B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;CAAC,EAClC;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,IAAI;CAAC,EACxC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,GAAG,CAC9B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;CAAC,EAC5B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,YAAY;IAAE,WAAW;CAAC,EACvD;IACE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB,CACF,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACpD,KAAK,OAAO,GAAG,sBAAsB,EAAE,CAAC;AACxC,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEnE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAyCnE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAQpE;AA2CD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAoGrE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAUtE"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts new file mode 100644 index 00000000..5f35e22f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts @@ -0,0 +1,9 @@ +export { decodeOriginalScopes, encodeOriginalScopes, decodeGeneratedRanges, encodeGeneratedRanges, } from './scopes.cts'; +export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes.cts'; +export type SourceMapSegment = [number] | [number, number, number, number] | [number, number, number, number, number]; +export type SourceMapLine = SourceMapSegment[]; +export type SourceMapMappings = SourceMapLine[]; +export declare function decode(mappings: string): SourceMapMappings; +export declare function encode(decoded: SourceMapMappings): string; +export declare function encode(decoded: Readonly): string; +//# sourceMappingURL=sourcemap-codec.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts.map new file mode 100644 index 00000000..7123d520 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"sourcemap-codec.d.ts","sourceRoot":"","sources":["../src/sourcemap-codec.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEhG,MAAM,MAAM,gBAAgB,GACxB,CAAC,MAAM,CAAC,GACR,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAChC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,MAAM,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,aAAa,EAAE,CAAC;AAEhD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAiD1D;AAUD,wBAAgB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;AAC3D,wBAAgB,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts new file mode 100644 index 00000000..199fb9f5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts @@ -0,0 +1,9 @@ +export { decodeOriginalScopes, encodeOriginalScopes, decodeGeneratedRanges, encodeGeneratedRanges, } from './scopes.mts'; +export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes.mts'; +export type SourceMapSegment = [number] | [number, number, number, number] | [number, number, number, number, number]; +export type SourceMapLine = SourceMapSegment[]; +export type SourceMapMappings = SourceMapLine[]; +export declare function decode(mappings: string): SourceMapMappings; +export declare function encode(decoded: SourceMapMappings): string; +export declare function encode(decoded: Readonly): string; +//# sourceMappingURL=sourcemap-codec.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts.map new file mode 100644 index 00000000..7123d520 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"sourcemap-codec.d.ts","sourceRoot":"","sources":["../src/sourcemap-codec.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEhG,MAAM,MAAM,gBAAgB,GACxB,CAAC,MAAM,CAAC,GACR,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAChC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,MAAM,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,aAAa,EAAE,CAAC;AAEhD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAiD1D;AAUD,wBAAgB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;AAC3D,wBAAgB,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts new file mode 100644 index 00000000..62faceb3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts @@ -0,0 +1,16 @@ +export declare class StringWriter { + pos: number; + private out; + private buffer; + write(v: number): void; + flush(): string; +} +export declare class StringReader { + pos: number; + private buffer; + constructor(buffer: string); + next(): number; + peek(): number; + indexOf(char: string): number; +} +//# sourceMappingURL=strings.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts.map new file mode 100644 index 00000000..d3602da4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAuBA,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,MAAM,CAA6B;IAE3C,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAStB,KAAK,IAAI,MAAM;CAIhB;AAED,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,QAAgB,MAAM,CAAS;gBAEnB,MAAM,EAAE,MAAM;IAI1B,IAAI,IAAI,MAAM;IAId,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAK9B"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts new file mode 100644 index 00000000..62faceb3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts @@ -0,0 +1,16 @@ +export declare class StringWriter { + pos: number; + private out; + private buffer; + write(v: number): void; + flush(): string; +} +export declare class StringReader { + pos: number; + private buffer; + constructor(buffer: string); + next(): number; + peek(): number; + indexOf(char: string): number; +} +//# sourceMappingURL=strings.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts.map new file mode 100644 index 00000000..d3602da4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAuBA,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,MAAM,CAA6B;IAE3C,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAStB,KAAK,IAAI,MAAM;CAIhB;AAED,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,QAAgB,MAAM,CAAS;gBAEnB,MAAM,EAAE,MAAM;IAI1B,IAAI,IAAI,MAAM;IAId,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAK9B"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts new file mode 100644 index 00000000..dbd6602d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts @@ -0,0 +1,7 @@ +import type { StringReader, StringWriter } from './strings.cts'; +export declare const comma: number; +export declare const semicolon: number; +export declare function decodeInteger(reader: StringReader, relative: number): number; +export declare function encodeInteger(builder: StringWriter, num: number, relative: number): number; +export declare function hasMoreVlq(reader: StringReader, max: number): boolean; +//# sourceMappingURL=vlq.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts.map new file mode 100644 index 00000000..6fdc3569 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"vlq.d.ts","sourceRoot":"","sources":["../src/vlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,eAAO,MAAM,KAAK,QAAoB,CAAC;AACvC,eAAO,MAAM,SAAS,QAAoB,CAAC;AAY3C,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoB5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,WAG3D"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts new file mode 100644 index 00000000..2c739bc9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts @@ -0,0 +1,7 @@ +import type { StringReader, StringWriter } from './strings.mts'; +export declare const comma: number; +export declare const semicolon: number; +export declare function decodeInteger(reader: StringReader, relative: number): number; +export declare function encodeInteger(builder: StringWriter, num: number, relative: number): number; +export declare function hasMoreVlq(reader: StringReader, max: number): boolean; +//# sourceMappingURL=vlq.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts.map new file mode 100644 index 00000000..6fdc3569 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"vlq.d.ts","sourceRoot":"","sources":["../src/vlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,eAAO,MAAM,KAAK,QAAoB,CAAC;AACvC,eAAO,MAAM,SAAS,QAAoB,CAAC;AAY3C,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoB5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,WAG3D"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/LICENSE new file mode 100644 index 00000000..1f6ce94c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/LICENSE @@ -0,0 +1,19 @@ +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/README.md new file mode 100644 index 00000000..9fc0ed09 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/README.md @@ -0,0 +1,348 @@ +# @jridgewell/trace-mapping + +> Trace the original position through a source map + +`trace-mapping` allows you to take the line and column of an output file and trace it to the +original location in the source file through a source map. + +You may already be familiar with the [`source-map`][source-map] package's `SourceMapConsumer`. This +provides the same `originalPositionFor` and `generatedPositionFor` API, without requiring WASM. + +## Installation + +```sh +npm install @jridgewell/trace-mapping +``` + +## Usage + +```typescript +import { + TraceMap, + originalPositionFor, + generatedPositionFor, + sourceContentFor, + isIgnored, +} from '@jridgewell/trace-mapping'; + +const tracer = new TraceMap({ + version: 3, + sources: ['input.js'], + sourcesContent: ['content of input.js'], + names: ['foo'], + mappings: 'KAyCIA', + ignoreList: [], +}); + +// Lines start at line 1, columns at column 0. +const traced = originalPositionFor(tracer, { line: 1, column: 5 }); +assert.deepEqual(traced, { + source: 'input.js', + line: 42, + column: 4, + name: 'foo', +}); + +const content = sourceContentFor(tracer, traced.source); +assert.strictEqual(content, 'content for input.js'); + +const generated = generatedPositionFor(tracer, { + source: 'input.js', + line: 42, + column: 4, +}); +assert.deepEqual(generated, { + line: 1, + column: 5, +}); + +const ignored = isIgnored(tracer, 'input.js'); +assert.equal(ignored, false); +``` + +We also provide a lower level API to get the actual segment that matches our line and column. Unlike +`originalPositionFor`, `traceSegment` uses a 0-base for `line`: + +```typescript +import { traceSegment } from '@jridgewell/trace-mapping'; + +// line is 0-base. +const traced = traceSegment(tracer, /* line */ 0, /* column */ 5); + +// Segments are [outputColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] +// Again, line is 0-base and so is sourceLine +assert.deepEqual(traced, [5, 0, 41, 4, 0]); +``` + +### SectionedSourceMaps + +The sourcemap spec defines a special `sections` field that's designed to handle concatenation of +output code with associated sourcemaps. This type of sourcemap is rarely used (no major build tool +produces it), but if you are hand coding a concatenation you may need it. We provide an `AnyMap` +helper that can receive either a regular sourcemap or a `SectionedSourceMap` and returns a +`TraceMap` instance: + +```typescript +import { AnyMap } from '@jridgewell/trace-mapping'; +const fooOutput = 'foo'; +const barOutput = 'bar'; +const output = [fooOutput, barOutput].join('\n'); + +const sectioned = new AnyMap({ + version: 3, + sections: [ + { + // 0-base line and column + offset: { line: 0, column: 0 }, + // fooOutput's sourcemap + map: { + version: 3, + sources: ['foo.js'], + names: ['foo'], + mappings: 'AAAAA', + }, + }, + { + // barOutput's sourcemap will not affect the first line, only the second + offset: { line: 1, column: 0 }, + map: { + version: 3, + sources: ['bar.js'], + names: ['bar'], + mappings: 'AAAAA', + }, + }, + ], +}); + +const traced = originalPositionFor(sectioned, { + line: 2, + column: 0, +}); + +assert.deepEqual(traced, { + source: 'bar.js', + line: 1, + column: 0, + name: 'bar', +}); +``` + +## Benchmarks + +``` +node v20.10.0 + +amp.js.map - 45120 segments + +Memory Usage: +trace-mapping decoded 414164 bytes +trace-mapping encoded 6274352 bytes +source-map-js 10968904 bytes +source-map-0.6.1 17587160 bytes +source-map-0.8.0 8812155 bytes +Chrome dev tools 8672912 bytes +Smallest memory usage is trace-mapping decoded + +Init speed: +trace-mapping: decoded JSON input x 205 ops/sec ±0.19% (88 runs sampled) +trace-mapping: encoded JSON input x 405 ops/sec ±1.47% (88 runs sampled) +trace-mapping: decoded Object input x 4,645 ops/sec ±0.15% (98 runs sampled) +trace-mapping: encoded Object input x 458 ops/sec ±1.63% (91 runs sampled) +source-map-js: encoded Object input x 75.48 ops/sec ±1.64% (67 runs sampled) +source-map-0.6.1: encoded Object input x 39.37 ops/sec ±1.44% (53 runs sampled) +Chrome dev tools: encoded Object input x 150 ops/sec ±1.76% (79 runs sampled) +Fastest is trace-mapping: decoded Object input + +Trace speed (random): +trace-mapping: decoded originalPositionFor x 44,946 ops/sec ±0.16% (99 runs sampled) +trace-mapping: encoded originalPositionFor x 37,995 ops/sec ±1.81% (89 runs sampled) +source-map-js: encoded originalPositionFor x 9,230 ops/sec ±1.36% (93 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 8,057 ops/sec ±0.84% (96 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 28,198 ops/sec ±1.12% (91 runs sampled) +Chrome dev tools: encoded originalPositionFor x 46,276 ops/sec ±1.35% (95 runs sampled) +Fastest is Chrome dev tools: encoded originalPositionFor + +Trace speed (ascending): +trace-mapping: decoded originalPositionFor x 204,406 ops/sec ±0.19% (97 runs sampled) +trace-mapping: encoded originalPositionFor x 196,695 ops/sec ±0.24% (99 runs sampled) +source-map-js: encoded originalPositionFor x 11,948 ops/sec ±0.94% (99 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 10,730 ops/sec ±0.36% (100 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 51,427 ops/sec ±0.21% (98 runs sampled) +Chrome dev tools: encoded originalPositionFor x 162,615 ops/sec ±0.18% (98 runs sampled) +Fastest is trace-mapping: decoded originalPositionFor + + +*** + + +babel.min.js.map - 347793 segments + +Memory Usage: +trace-mapping decoded 18504 bytes +trace-mapping encoded 35428008 bytes +source-map-js 51676808 bytes +source-map-0.6.1 63367136 bytes +source-map-0.8.0 43158400 bytes +Chrome dev tools 50721552 bytes +Smallest memory usage is trace-mapping decoded + +Init speed: +trace-mapping: decoded JSON input x 17.82 ops/sec ±6.35% (35 runs sampled) +trace-mapping: encoded JSON input x 31.57 ops/sec ±7.50% (43 runs sampled) +trace-mapping: decoded Object input x 867 ops/sec ±0.74% (94 runs sampled) +trace-mapping: encoded Object input x 33.83 ops/sec ±7.66% (46 runs sampled) +source-map-js: encoded Object input x 6.58 ops/sec ±3.31% (20 runs sampled) +source-map-0.6.1: encoded Object input x 4.23 ops/sec ±3.43% (15 runs sampled) +Chrome dev tools: encoded Object input x 22.14 ops/sec ±3.79% (41 runs sampled) +Fastest is trace-mapping: decoded Object input + +Trace speed (random): +trace-mapping: decoded originalPositionFor x 78,234 ops/sec ±1.48% (29 runs sampled) +trace-mapping: encoded originalPositionFor x 60,761 ops/sec ±1.35% (21 runs sampled) +source-map-js: encoded originalPositionFor x 51,448 ops/sec ±2.17% (89 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 47,221 ops/sec ±1.99% (15 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 84,002 ops/sec ±1.45% (27 runs sampled) +Chrome dev tools: encoded originalPositionFor x 106,457 ops/sec ±1.38% (37 runs sampled) +Fastest is Chrome dev tools: encoded originalPositionFor + +Trace speed (ascending): +trace-mapping: decoded originalPositionFor x 930,943 ops/sec ±0.25% (99 runs sampled) +trace-mapping: encoded originalPositionFor x 843,545 ops/sec ±0.34% (97 runs sampled) +source-map-js: encoded originalPositionFor x 114,510 ops/sec ±1.37% (36 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 87,412 ops/sec ±0.72% (92 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 197,709 ops/sec ±0.89% (59 runs sampled) +Chrome dev tools: encoded originalPositionFor x 688,983 ops/sec ±0.33% (98 runs sampled) +Fastest is trace-mapping: decoded originalPositionFor + + +*** + + +preact.js.map - 1992 segments + +Memory Usage: +trace-mapping decoded 33136 bytes +trace-mapping encoded 254240 bytes +source-map-js 837488 bytes +source-map-0.6.1 961928 bytes +source-map-0.8.0 54384 bytes +Chrome dev tools 709680 bytes +Smallest memory usage is trace-mapping decoded + +Init speed: +trace-mapping: decoded JSON input x 3,709 ops/sec ±0.13% (99 runs sampled) +trace-mapping: encoded JSON input x 6,447 ops/sec ±0.22% (101 runs sampled) +trace-mapping: decoded Object input x 83,062 ops/sec ±0.23% (100 runs sampled) +trace-mapping: encoded Object input x 14,980 ops/sec ±0.28% (100 runs sampled) +source-map-js: encoded Object input x 2,544 ops/sec ±0.16% (99 runs sampled) +source-map-0.6.1: encoded Object input x 1,221 ops/sec ±0.37% (97 runs sampled) +Chrome dev tools: encoded Object input x 4,241 ops/sec ±0.39% (93 runs sampled) +Fastest is trace-mapping: decoded Object input + +Trace speed (random): +trace-mapping: decoded originalPositionFor x 91,028 ops/sec ±0.14% (94 runs sampled) +trace-mapping: encoded originalPositionFor x 84,348 ops/sec ±0.26% (98 runs sampled) +source-map-js: encoded originalPositionFor x 26,998 ops/sec ±0.23% (98 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 18,049 ops/sec ±0.26% (100 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 41,916 ops/sec ±0.28% (98 runs sampled) +Chrome dev tools: encoded originalPositionFor x 88,616 ops/sec ±0.14% (98 runs sampled) +Fastest is trace-mapping: decoded originalPositionFor + +Trace speed (ascending): +trace-mapping: decoded originalPositionFor x 319,960 ops/sec ±0.16% (100 runs sampled) +trace-mapping: encoded originalPositionFor x 302,153 ops/sec ±0.18% (100 runs sampled) +source-map-js: encoded originalPositionFor x 35,574 ops/sec ±0.19% (100 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 19,943 ops/sec ±0.12% (101 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 54,648 ops/sec ±0.20% (99 runs sampled) +Chrome dev tools: encoded originalPositionFor x 278,319 ops/sec ±0.17% (102 runs sampled) +Fastest is trace-mapping: decoded originalPositionFor + + +*** + + +react.js.map - 5726 segments + +Memory Usage: +trace-mapping decoded 10872 bytes +trace-mapping encoded 681512 bytes +source-map-js 2563944 bytes +source-map-0.6.1 2150864 bytes +source-map-0.8.0 88680 bytes +Chrome dev tools 1149576 bytes +Smallest memory usage is trace-mapping decoded + +Init speed: +trace-mapping: decoded JSON input x 1,887 ops/sec ±0.28% (99 runs sampled) +trace-mapping: encoded JSON input x 4,749 ops/sec ±0.48% (97 runs sampled) +trace-mapping: decoded Object input x 74,236 ops/sec ±0.11% (99 runs sampled) +trace-mapping: encoded Object input x 5,752 ops/sec ±0.38% (100 runs sampled) +source-map-js: encoded Object input x 806 ops/sec ±0.19% (97 runs sampled) +source-map-0.6.1: encoded Object input x 418 ops/sec ±0.33% (94 runs sampled) +Chrome dev tools: encoded Object input x 1,524 ops/sec ±0.57% (92 runs sampled) +Fastest is trace-mapping: decoded Object input + +Trace speed (random): +trace-mapping: decoded originalPositionFor x 620,201 ops/sec ±0.33% (96 runs sampled) +trace-mapping: encoded originalPositionFor x 579,548 ops/sec ±0.35% (97 runs sampled) +source-map-js: encoded originalPositionFor x 230,983 ops/sec ±0.62% (54 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 158,145 ops/sec ±0.80% (46 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 343,801 ops/sec ±0.55% (96 runs sampled) +Chrome dev tools: encoded originalPositionFor x 659,649 ops/sec ±0.49% (98 runs sampled) +Fastest is Chrome dev tools: encoded originalPositionFor + +Trace speed (ascending): +trace-mapping: decoded originalPositionFor x 2,368,079 ops/sec ±0.32% (98 runs sampled) +trace-mapping: encoded originalPositionFor x 2,134,039 ops/sec ±2.72% (87 runs sampled) +source-map-js: encoded originalPositionFor x 290,120 ops/sec ±2.49% (82 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 187,613 ops/sec ±0.86% (49 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 479,569 ops/sec ±0.65% (96 runs sampled) +Chrome dev tools: encoded originalPositionFor x 2,048,414 ops/sec ±0.24% (98 runs sampled) +Fastest is trace-mapping: decoded originalPositionFor + + +*** + + +vscode.map - 2141001 segments + +Memory Usage: +trace-mapping decoded 5206584 bytes +trace-mapping encoded 208370336 bytes +source-map-js 278493008 bytes +source-map-0.6.1 391564048 bytes +source-map-0.8.0 257508787 bytes +Chrome dev tools 291053000 bytes +Smallest memory usage is trace-mapping decoded + +Init speed: +trace-mapping: decoded JSON input x 1.63 ops/sec ±33.88% (9 runs sampled) +trace-mapping: encoded JSON input x 3.29 ops/sec ±36.13% (13 runs sampled) +trace-mapping: decoded Object input x 103 ops/sec ±0.93% (77 runs sampled) +trace-mapping: encoded Object input x 5.42 ops/sec ±28.54% (19 runs sampled) +source-map-js: encoded Object input x 1.07 ops/sec ±13.84% (7 runs sampled) +source-map-0.6.1: encoded Object input x 0.60 ops/sec ±2.43% (6 runs sampled) +Chrome dev tools: encoded Object input x 2.61 ops/sec ±22.00% (11 runs sampled) +Fastest is trace-mapping: decoded Object input + +Trace speed (random): +trace-mapping: decoded originalPositionFor x 257,019 ops/sec ±0.97% (93 runs sampled) +trace-mapping: encoded originalPositionFor x 179,163 ops/sec ±0.83% (92 runs sampled) +source-map-js: encoded originalPositionFor x 73,337 ops/sec ±1.35% (87 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 38,797 ops/sec ±1.66% (88 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 107,758 ops/sec ±1.94% (45 runs sampled) +Chrome dev tools: encoded originalPositionFor x 188,550 ops/sec ±1.85% (79 runs sampled) +Fastest is trace-mapping: decoded originalPositionFor + +Trace speed (ascending): +trace-mapping: decoded originalPositionFor x 447,621 ops/sec ±3.64% (94 runs sampled) +trace-mapping: encoded originalPositionFor x 323,698 ops/sec ±5.20% (88 runs sampled) +source-map-js: encoded originalPositionFor x 78,387 ops/sec ±1.69% (89 runs sampled) +source-map-0.6.1: encoded originalPositionFor x 41,016 ops/sec ±3.01% (25 runs sampled) +source-map-0.8.0: encoded originalPositionFor x 124,204 ops/sec ±0.90% (92 runs sampled) +Chrome dev tools: encoded originalPositionFor x 230,087 ops/sec ±2.61% (93 runs sampled) +Fastest is trace-mapping: decoded originalPositionFor +``` + +[source-map]: https://www.npmjs.com/package/source-map diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/package.json new file mode 100644 index 00000000..9d3a1c08 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/package.json @@ -0,0 +1,67 @@ +{ + "name": "@jridgewell/trace-mapping", + "version": "0.3.31", + "description": "Trace the original position through a source map", + "keywords": [ + "source", + "map" + ], + "main": "dist/trace-mapping.umd.js", + "module": "dist/trace-mapping.mjs", + "types": "types/trace-mapping.d.cts", + "files": [ + "dist", + "src", + "types" + ], + "exports": { + ".": [ + { + "import": { + "types": "./types/trace-mapping.d.mts", + "default": "./dist/trace-mapping.mjs" + }, + "default": { + "types": "./types/trace-mapping.d.cts", + "default": "./dist/trace-mapping.umd.js" + } + }, + "./dist/trace-mapping.umd.js" + ], + "./package.json": "./package.json" + }, + "scripts": { + "benchmark": "run-s build:code benchmark:*", + "benchmark:install": "cd benchmark && npm install", + "benchmark:only": "node --expose-gc benchmark/index.mjs", + "build": "run-s -n build:code build:types", + "build:code": "node ../../esbuild.mjs trace-mapping.ts", + "build:types": "run-s build:types:force build:types:emit build:types:mts", + "build:types:force": "rimraf tsconfig.build.tsbuildinfo", + "build:types:emit": "tsc --project tsconfig.build.json", + "build:types:mts": "node ../../mts-types.mjs", + "clean": "run-s -n clean:code clean:types", + "clean:code": "tsc --build --clean tsconfig.build.json", + "clean:types": "rimraf dist types", + "test": "run-s -n test:types test:only test:format", + "test:format": "prettier --check '{src,test}/**/*.ts'", + "test:only": "mocha", + "test:types": "eslint '{src,test}/**/*.ts'", + "lint": "run-s -n lint:types lint:format", + "lint:format": "npm run test:format -- --write", + "lint:types": "npm run test:types -- --fix", + "prepublishOnly": "npm run-s -n build test" + }, + "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping", + "repository": { + "type": "git", + "url": "git+https://github.com/jridgewell/sourcemaps.git", + "directory": "packages/trace-mapping" + }, + "author": "Justin Ridgewell ", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/binary-search.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/binary-search.ts new file mode 100644 index 00000000..c1144ad1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/binary-search.ts @@ -0,0 +1,115 @@ +import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment'; +import { COLUMN } from './sourcemap-segment'; + +export type MemoState = { + lastKey: number; + lastNeedle: number; + lastIndex: number; +}; + +export let found = false; + +/** + * A binary search implementation that returns the index if a match is found. + * If no match is found, then the left-index (the index associated with the item that comes just + * before the desired index) is returned. To maintain proper sort order, a splice would happen at + * the next index: + * + * ```js + * const array = [1, 3]; + * const needle = 2; + * const index = binarySearch(array, needle, (item, needle) => item - needle); + * + * assert.equal(index, 0); + * array.splice(index + 1, 0, needle); + * assert.deepEqual(array, [1, 2, 3]); + * ``` + */ +export function binarySearch( + haystack: SourceMapSegment[] | ReverseSegment[], + needle: number, + low: number, + high: number, +): number { + while (low <= high) { + const mid = low + ((high - low) >> 1); + const cmp = haystack[mid][COLUMN] - needle; + + if (cmp === 0) { + found = true; + return mid; + } + + if (cmp < 0) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + found = false; + return low - 1; +} + +export function upperBound( + haystack: SourceMapSegment[] | ReverseSegment[], + needle: number, + index: number, +): number { + for (let i = index + 1; i < haystack.length; index = i++) { + if (haystack[i][COLUMN] !== needle) break; + } + return index; +} + +export function lowerBound( + haystack: SourceMapSegment[] | ReverseSegment[], + needle: number, + index: number, +): number { + for (let i = index - 1; i >= 0; index = i--) { + if (haystack[i][COLUMN] !== needle) break; + } + return index; +} + +export function memoizedState(): MemoState { + return { + lastKey: -1, + lastNeedle: -1, + lastIndex: -1, + }; +} + +/** + * This overly complicated beast is just to record the last tested line/column and the resulting + * index, allowing us to skip a few tests if mappings are monotonically increasing. + */ +export function memoizedBinarySearch( + haystack: SourceMapSegment[] | ReverseSegment[], + needle: number, + state: MemoState, + key: number, +): number { + const { lastKey, lastNeedle, lastIndex } = state; + + let low = 0; + let high = haystack.length - 1; + if (key === lastKey) { + if (needle === lastNeedle) { + found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle; + return lastIndex; + } + + if (needle >= lastNeedle) { + // lastIndex may be -1 if the previous needle was not found. + low = lastIndex === -1 ? 0 : lastIndex; + } else { + high = lastIndex; + } + } + state.lastKey = key; + state.lastNeedle = needle; + + return (state.lastIndex = binarySearch(haystack, needle, low, high)); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/by-source.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/by-source.ts new file mode 100644 index 00000000..1da6af05 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/by-source.ts @@ -0,0 +1,41 @@ +import { COLUMN, SOURCES_INDEX, SOURCE_LINE, SOURCE_COLUMN } from './sourcemap-segment'; +import { sortComparator } from './sort'; + +import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment'; + +export type Source = ReverseSegment[][]; + +// Rebuilds the original source files, with mappings that are ordered by source line/column instead +// of generated line/column. +export default function buildBySources( + decoded: readonly SourceMapSegment[][], + memos: unknown[], +): Source[] { + const sources: Source[] = memos.map(() => []); + + for (let i = 0; i < decoded.length; i++) { + const line = decoded[i]; + for (let j = 0; j < line.length; j++) { + const seg = line[j]; + if (seg.length === 1) continue; + + const sourceIndex = seg[SOURCES_INDEX]; + const sourceLine = seg[SOURCE_LINE]; + const sourceColumn = seg[SOURCE_COLUMN]; + + const source = sources[sourceIndex]; + const segs = (source[sourceLine] ||= []); + segs.push([sourceColumn, i, seg[COLUMN]]); + } + } + + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + for (let j = 0; j < source.length; j++) { + const line = source[j]; + if (line) line.sort(sortComparator); + } + } + + return sources; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/flatten-map.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/flatten-map.ts new file mode 100644 index 00000000..61ac40ca --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/flatten-map.ts @@ -0,0 +1,192 @@ +import { TraceMap, presortedDecodedMap, decodedMappings } from './trace-mapping'; +import { + COLUMN, + SOURCES_INDEX, + SOURCE_LINE, + SOURCE_COLUMN, + NAMES_INDEX, +} from './sourcemap-segment'; +import { parse } from './types'; + +import type { + DecodedSourceMap, + DecodedSourceMapXInput, + EncodedSourceMapXInput, + SectionedSourceMapXInput, + SectionedSourceMapInput, + SectionXInput, + Ro, +} from './types'; +import type { SourceMapSegment } from './sourcemap-segment'; + +type FlattenMap = { + new (map: Ro, mapUrl?: string | null): TraceMap; + (map: Ro, mapUrl?: string | null): TraceMap; +}; + +export const FlattenMap: FlattenMap = function (map, mapUrl) { + const parsed = parse(map as SectionedSourceMapInput); + + if (!('sections' in parsed)) { + return new TraceMap(parsed as DecodedSourceMapXInput | EncodedSourceMapXInput, mapUrl); + } + + const mappings: SourceMapSegment[][] = []; + const sources: string[] = []; + const sourcesContent: (string | null)[] = []; + const names: string[] = []; + const ignoreList: number[] = []; + + recurse( + parsed, + mapUrl, + mappings, + sources, + sourcesContent, + names, + ignoreList, + 0, + 0, + Infinity, + Infinity, + ); + + const joined: DecodedSourceMap = { + version: 3, + file: parsed.file, + names, + sources, + sourcesContent, + mappings, + ignoreList, + }; + + return presortedDecodedMap(joined); +} as FlattenMap; + +function recurse( + input: SectionedSourceMapXInput, + mapUrl: string | null | undefined, + mappings: SourceMapSegment[][], + sources: string[], + sourcesContent: (string | null)[], + names: string[], + ignoreList: number[], + lineOffset: number, + columnOffset: number, + stopLine: number, + stopColumn: number, +) { + const { sections } = input; + for (let i = 0; i < sections.length; i++) { + const { map, offset } = sections[i]; + + let sl = stopLine; + let sc = stopColumn; + if (i + 1 < sections.length) { + const nextOffset = sections[i + 1].offset; + sl = Math.min(stopLine, lineOffset + nextOffset.line); + + if (sl === stopLine) { + sc = Math.min(stopColumn, columnOffset + nextOffset.column); + } else if (sl < stopLine) { + sc = columnOffset + nextOffset.column; + } + } + + addSection( + map, + mapUrl, + mappings, + sources, + sourcesContent, + names, + ignoreList, + lineOffset + offset.line, + columnOffset + offset.column, + sl, + sc, + ); + } +} + +function addSection( + input: SectionXInput['map'], + mapUrl: string | null | undefined, + mappings: SourceMapSegment[][], + sources: string[], + sourcesContent: (string | null)[], + names: string[], + ignoreList: number[], + lineOffset: number, + columnOffset: number, + stopLine: number, + stopColumn: number, +) { + const parsed = parse(input); + if ('sections' in parsed) return recurse(...(arguments as unknown as Parameters)); + + const map = new TraceMap(parsed, mapUrl); + const sourcesOffset = sources.length; + const namesOffset = names.length; + const decoded = decodedMappings(map); + const { resolvedSources, sourcesContent: contents, ignoreList: ignores } = map; + + append(sources, resolvedSources); + append(names, map.names); + + if (contents) append(sourcesContent, contents); + else for (let i = 0; i < resolvedSources.length; i++) sourcesContent.push(null); + + if (ignores) for (let i = 0; i < ignores.length; i++) ignoreList.push(ignores[i] + sourcesOffset); + + for (let i = 0; i < decoded.length; i++) { + const lineI = lineOffset + i; + + // We can only add so many lines before we step into the range that the next section's map + // controls. When we get to the last line, then we'll start checking the segments to see if + // they've crossed into the column range. But it may not have any columns that overstep, so we + // still need to check that we don't overstep lines, too. + if (lineI > stopLine) return; + + // The out line may already exist in mappings (if we're continuing the line started by a + // previous section). Or, we may have jumped ahead several lines to start this section. + const out = getLine(mappings, lineI); + // On the 0th loop, the section's column offset shifts us forward. On all other lines (since the + // map can be multiple lines), it doesn't. + const cOffset = i === 0 ? columnOffset : 0; + + const line = decoded[i]; + for (let j = 0; j < line.length; j++) { + const seg = line[j]; + const column = cOffset + seg[COLUMN]; + + // If this segment steps into the column range that the next section's map controls, we need + // to stop early. + if (lineI === stopLine && column >= stopColumn) return; + + if (seg.length === 1) { + out.push([column]); + continue; + } + + const sourcesIndex = sourcesOffset + seg[SOURCES_INDEX]; + const sourceLine = seg[SOURCE_LINE]; + const sourceColumn = seg[SOURCE_COLUMN]; + out.push( + seg.length === 4 + ? [column, sourcesIndex, sourceLine, sourceColumn] + : [column, sourcesIndex, sourceLine, sourceColumn, namesOffset + seg[NAMES_INDEX]], + ); + } + } +} + +function append(arr: T[], other: T[]) { + for (let i = 0; i < other.length; i++) arr.push(other[i]); +} + +function getLine(arr: T[][], index: number): T[] { + for (let i = arr.length; i <= index; i++) arr[i] = []; + return arr[index]; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/resolve.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/resolve.ts new file mode 100644 index 00000000..30bfa3b2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/resolve.ts @@ -0,0 +1,16 @@ +import resolveUri from '@jridgewell/resolve-uri'; +import stripFilename from './strip-filename'; + +type Resolve = (source: string | null) => string; +export default function resolver( + mapUrl: string | null | undefined, + sourceRoot: string | undefined, +): Resolve { + const from = stripFilename(mapUrl); + // The sourceRoot is always treated as a directory, if it's not empty. + // https://github.com/mozilla/source-map/blob/8cb3ee57/lib/util.js#L327 + // https://github.com/chromium/chromium/blob/da4adbb3/third_party/blink/renderer/devtools/front_end/sdk/SourceMap.js#L400-L401 + const prefix = sourceRoot ? sourceRoot + '/' : ''; + + return (source) => resolveUri(prefix + (source || ''), from); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sort.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sort.ts new file mode 100644 index 00000000..5d016cb7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sort.ts @@ -0,0 +1,45 @@ +import { COLUMN } from './sourcemap-segment'; + +import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment'; + +export default function maybeSort( + mappings: SourceMapSegment[][], + owned: boolean, +): SourceMapSegment[][] { + const unsortedIndex = nextUnsortedSegmentLine(mappings, 0); + if (unsortedIndex === mappings.length) return mappings; + + // If we own the array (meaning we parsed it from JSON), then we're free to directly mutate it. If + // not, we do not want to modify the consumer's input array. + if (!owned) mappings = mappings.slice(); + + for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) { + mappings[i] = sortSegments(mappings[i], owned); + } + return mappings; +} + +function nextUnsortedSegmentLine(mappings: SourceMapSegment[][], start: number): number { + for (let i = start; i < mappings.length; i++) { + if (!isSorted(mappings[i])) return i; + } + return mappings.length; +} + +function isSorted(line: SourceMapSegment[]): boolean { + for (let j = 1; j < line.length; j++) { + if (line[j][COLUMN] < line[j - 1][COLUMN]) { + return false; + } + } + return true; +} + +function sortSegments(line: SourceMapSegment[], owned: boolean): SourceMapSegment[] { + if (!owned) line = line.slice(); + return line.sort(sortComparator); +} + +export function sortComparator(a: T, b: T): number { + return a[COLUMN] - b[COLUMN]; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sourcemap-segment.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sourcemap-segment.ts new file mode 100644 index 00000000..94f1b6ab --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sourcemap-segment.ts @@ -0,0 +1,23 @@ +type GeneratedColumn = number; +type SourcesIndex = number; +type SourceLine = number; +type SourceColumn = number; +type NamesIndex = number; + +type GeneratedLine = number; + +export type SourceMapSegment = + | [GeneratedColumn] + | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] + | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; + +export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn]; + +export const COLUMN = 0; +export const SOURCES_INDEX = 1; +export const SOURCE_LINE = 2; +export const SOURCE_COLUMN = 3; +export const NAMES_INDEX = 4; + +export const REV_GENERATED_LINE = 1; +export const REV_GENERATED_COLUMN = 2; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/strip-filename.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/strip-filename.ts new file mode 100644 index 00000000..2c889800 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/strip-filename.ts @@ -0,0 +1,8 @@ +/** + * Removes everything after the last "/", but leaves the slash. + */ +export default function stripFilename(path: string | undefined | null): string { + if (!path) return ''; + const index = path.lastIndexOf('/'); + return path.slice(0, index + 1); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/trace-mapping.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/trace-mapping.ts new file mode 100644 index 00000000..0b793d5b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/trace-mapping.ts @@ -0,0 +1,502 @@ +import { encode, decode } from '@jridgewell/sourcemap-codec'; + +import resolver from './resolve'; +import maybeSort from './sort'; +import buildBySources from './by-source'; +import { + memoizedState, + memoizedBinarySearch, + upperBound, + lowerBound, + found as bsFound, +} from './binary-search'; +import { + COLUMN, + SOURCES_INDEX, + SOURCE_LINE, + SOURCE_COLUMN, + NAMES_INDEX, + REV_GENERATED_LINE, + REV_GENERATED_COLUMN, +} from './sourcemap-segment'; +import { parse } from './types'; + +import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment'; +import type { + SourceMapV3, + DecodedSourceMap, + EncodedSourceMap, + InvalidOriginalMapping, + OriginalMapping, + InvalidGeneratedMapping, + GeneratedMapping, + SourceMapInput, + Needle, + SourceNeedle, + SourceMap, + EachMapping, + Bias, + XInput, + SectionedSourceMap, + Ro, +} from './types'; +import type { Source } from './by-source'; +import type { MemoState } from './binary-search'; + +export type { SourceMapSegment } from './sourcemap-segment'; +export type { + SourceMap, + DecodedSourceMap, + EncodedSourceMap, + Section, + SectionedSourceMap, + SourceMapV3, + Bias, + EachMapping, + GeneratedMapping, + InvalidGeneratedMapping, + InvalidOriginalMapping, + Needle, + OriginalMapping, + OriginalMapping as Mapping, + SectionedSourceMapInput, + SourceMapInput, + SourceNeedle, + XInput, + EncodedSourceMapXInput, + DecodedSourceMapXInput, + SectionedSourceMapXInput, + SectionXInput, +} from './types'; + +interface PublicMap { + _encoded: TraceMap['_encoded']; + _decoded: TraceMap['_decoded']; + _decodedMemo: TraceMap['_decodedMemo']; + _bySources: TraceMap['_bySources']; + _bySourceMemos: TraceMap['_bySourceMemos']; +} + +const LINE_GTR_ZERO = '`line` must be greater than 0 (lines start at line 1)'; +const COL_GTR_EQ_ZERO = '`column` must be greater than or equal to 0 (columns start at column 0)'; + +export const LEAST_UPPER_BOUND = -1; +export const GREATEST_LOWER_BOUND = 1; + +export { FlattenMap, FlattenMap as AnyMap } from './flatten-map'; + +export class TraceMap implements SourceMap { + declare version: SourceMapV3['version']; + declare file: SourceMapV3['file']; + declare names: SourceMapV3['names']; + declare sourceRoot: SourceMapV3['sourceRoot']; + declare sources: SourceMapV3['sources']; + declare sourcesContent: SourceMapV3['sourcesContent']; + declare ignoreList: SourceMapV3['ignoreList']; + + declare resolvedSources: string[]; + declare private _encoded: string | undefined; + + declare private _decoded: SourceMapSegment[][] | undefined; + declare private _decodedMemo: MemoState; + + declare private _bySources: Source[] | undefined; + declare private _bySourceMemos: MemoState[] | undefined; + + constructor(map: Ro, mapUrl?: string | null) { + const isString = typeof map === 'string'; + if (!isString && (map as unknown as { _decodedMemo: any })._decodedMemo) return map as TraceMap; + + const parsed = parse(map as Exclude); + + const { version, file, names, sourceRoot, sources, sourcesContent } = parsed; + this.version = version; + this.file = file; + this.names = names || []; + this.sourceRoot = sourceRoot; + this.sources = sources; + this.sourcesContent = sourcesContent; + this.ignoreList = parsed.ignoreList || (parsed as XInput).x_google_ignoreList || undefined; + + const resolve = resolver(mapUrl, sourceRoot); + this.resolvedSources = sources.map(resolve); + + const { mappings } = parsed; + if (typeof mappings === 'string') { + this._encoded = mappings; + this._decoded = undefined; + } else if (Array.isArray(mappings)) { + this._encoded = undefined; + this._decoded = maybeSort(mappings, isString); + } else if ((parsed as unknown as SectionedSourceMap).sections) { + throw new Error(`TraceMap passed sectioned source map, please use FlattenMap export instead`); + } else { + throw new Error(`invalid source map: ${JSON.stringify(parsed)}`); + } + + this._decodedMemo = memoizedState(); + this._bySources = undefined; + this._bySourceMemos = undefined; + } +} + +/** + * Typescript doesn't allow friend access to private fields, so this just casts the map into a type + * with public access modifiers. + */ +function cast(map: unknown): PublicMap { + return map as any; +} + +/** + * Returns the encoded (VLQ string) form of the SourceMap's mappings field. + */ +export function encodedMappings(map: TraceMap): EncodedSourceMap['mappings'] { + return (cast(map)._encoded ??= encode(cast(map)._decoded!)); +} + +/** + * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field. + */ +export function decodedMappings(map: TraceMap): Readonly { + return (cast(map)._decoded ||= decode(cast(map)._encoded!)); +} + +/** + * A low-level API to find the segment associated with a generated line/column (think, from a + * stack trace). Line and column here are 0-based, unlike `originalPositionFor`. + */ +export function traceSegment( + map: TraceMap, + line: number, + column: number, +): Readonly | null { + const decoded = decodedMappings(map); + + // It's common for parent source maps to have pointers to lines that have no + // mapping (like a "//# sourceMappingURL=") at the end of the child file. + if (line >= decoded.length) return null; + + const segments = decoded[line]; + const index = traceSegmentInternal( + segments, + cast(map)._decodedMemo, + line, + column, + GREATEST_LOWER_BOUND, + ); + + return index === -1 ? null : segments[index]; +} + +/** + * A higher-level API to find the source/line/column associated with a generated line/column + * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in + * `source-map` library. + */ +export function originalPositionFor( + map: TraceMap, + needle: Needle, +): OriginalMapping | InvalidOriginalMapping { + let { line, column, bias } = needle; + line--; + if (line < 0) throw new Error(LINE_GTR_ZERO); + if (column < 0) throw new Error(COL_GTR_EQ_ZERO); + + const decoded = decodedMappings(map); + + // It's common for parent source maps to have pointers to lines that have no + // mapping (like a "//# sourceMappingURL=") at the end of the child file. + if (line >= decoded.length) return OMapping(null, null, null, null); + + const segments = decoded[line]; + const index = traceSegmentInternal( + segments, + cast(map)._decodedMemo, + line, + column, + bias || GREATEST_LOWER_BOUND, + ); + + if (index === -1) return OMapping(null, null, null, null); + + const segment = segments[index]; + if (segment.length === 1) return OMapping(null, null, null, null); + + const { names, resolvedSources } = map; + return OMapping( + resolvedSources[segment[SOURCES_INDEX]], + segment[SOURCE_LINE] + 1, + segment[SOURCE_COLUMN], + segment.length === 5 ? names[segment[NAMES_INDEX]] : null, + ); +} + +/** + * Finds the generated line/column position of the provided source/line/column source position. + */ +export function generatedPositionFor( + map: TraceMap, + needle: SourceNeedle, +): GeneratedMapping | InvalidGeneratedMapping { + const { source, line, column, bias } = needle; + return generatedPosition(map, source, line, column, bias || GREATEST_LOWER_BOUND, false); +} + +/** + * Finds all generated line/column positions of the provided source/line/column source position. + */ +export function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[] { + const { source, line, column, bias } = needle; + // SourceMapConsumer uses LEAST_UPPER_BOUND for some reason, so we follow suit. + return generatedPosition(map, source, line, column, bias || LEAST_UPPER_BOUND, true); +} + +/** + * Iterates each mapping in generated position order. + */ +export function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void { + const decoded = decodedMappings(map); + const { names, resolvedSources } = map; + + for (let i = 0; i < decoded.length; i++) { + const line = decoded[i]; + for (let j = 0; j < line.length; j++) { + const seg = line[j]; + + const generatedLine = i + 1; + const generatedColumn = seg[0]; + let source = null; + let originalLine = null; + let originalColumn = null; + let name = null; + if (seg.length !== 1) { + source = resolvedSources[seg[1]]; + originalLine = seg[2] + 1; + originalColumn = seg[3]; + } + if (seg.length === 5) name = names[seg[4]]; + + cb({ + generatedLine, + generatedColumn, + source, + originalLine, + originalColumn, + name, + } as EachMapping); + } + } +} + +function sourceIndex(map: TraceMap, source: string): number { + const { sources, resolvedSources } = map; + let index = sources.indexOf(source); + if (index === -1) index = resolvedSources.indexOf(source); + return index; +} + +/** + * Retrieves the source content for a particular source, if its found. Returns null if not. + */ +export function sourceContentFor(map: TraceMap, source: string): string | null { + const { sourcesContent } = map; + if (sourcesContent == null) return null; + const index = sourceIndex(map, source); + return index === -1 ? null : sourcesContent[index]; +} + +/** + * Determines if the source is marked to ignore by the source map. + */ +export function isIgnored(map: TraceMap, source: string): boolean { + const { ignoreList } = map; + if (ignoreList == null) return false; + const index = sourceIndex(map, source); + return index === -1 ? false : ignoreList.includes(index); +} + +/** + * A helper that skips sorting of the input map's mappings array, which can be expensive for larger + * maps. + */ +export function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap { + const tracer = new TraceMap(clone(map, []), mapUrl); + cast(tracer)._decoded = map.mappings; + return tracer; +} + +/** + * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export function decodedMap( + map: TraceMap, +): Omit & { mappings: readonly SourceMapSegment[][] } { + return clone(map, decodedMappings(map)); +} + +/** + * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export function encodedMap(map: TraceMap): EncodedSourceMap { + return clone(map, encodedMappings(map)); +} + +function clone( + map: TraceMap | DecodedSourceMap, + mappings: T, +): T extends string ? EncodedSourceMap : DecodedSourceMap { + return { + version: map.version, + file: map.file, + names: map.names, + sourceRoot: map.sourceRoot, + sources: map.sources, + sourcesContent: map.sourcesContent, + mappings, + ignoreList: map.ignoreList || (map as XInput).x_google_ignoreList, + } as any; +} + +function OMapping(source: null, line: null, column: null, name: null): InvalidOriginalMapping; +function OMapping( + source: string, + line: number, + column: number, + name: string | null, +): OriginalMapping; +function OMapping( + source: string | null, + line: number | null, + column: number | null, + name: string | null, +): OriginalMapping | InvalidOriginalMapping { + return { source, line, column, name } as any; +} + +function GMapping(line: null, column: null): InvalidGeneratedMapping; +function GMapping(line: number, column: number): GeneratedMapping; +function GMapping( + line: number | null, + column: number | null, +): GeneratedMapping | InvalidGeneratedMapping { + return { line, column } as any; +} + +function traceSegmentInternal( + segments: SourceMapSegment[], + memo: MemoState, + line: number, + column: number, + bias: Bias, +): number; +function traceSegmentInternal( + segments: ReverseSegment[], + memo: MemoState, + line: number, + column: number, + bias: Bias, +): number; +function traceSegmentInternal( + segments: SourceMapSegment[] | ReverseSegment[], + memo: MemoState, + line: number, + column: number, + bias: Bias, +): number { + let index = memoizedBinarySearch(segments, column, memo, line); + if (bsFound) { + index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index); + } else if (bias === LEAST_UPPER_BOUND) index++; + + if (index === -1 || index === segments.length) return -1; + return index; +} + +function sliceGeneratedPositions( + segments: ReverseSegment[], + memo: MemoState, + line: number, + column: number, + bias: Bias, +): GeneratedMapping[] { + let min = traceSegmentInternal(segments, memo, line, column, GREATEST_LOWER_BOUND); + + // We ignored the bias when tracing the segment so that we're guarnateed to find the first (in + // insertion order) segment that matched. Even if we did respect the bias when tracing, we would + // still need to call `lowerBound()` to find the first segment, which is slower than just looking + // for the GREATEST_LOWER_BOUND to begin with. The only difference that matters for us is when the + // binary search didn't match, in which case GREATEST_LOWER_BOUND just needs to increment to + // match LEAST_UPPER_BOUND. + if (!bsFound && bias === LEAST_UPPER_BOUND) min++; + + if (min === -1 || min === segments.length) return []; + + // We may have found the segment that started at an earlier column. If this is the case, then we + // need to slice all generated segments that match _that_ column, because all such segments span + // to our desired column. + const matchedColumn = bsFound ? column : segments[min][COLUMN]; + + // The binary search is not guaranteed to find the lower bound when a match wasn't found. + if (!bsFound) min = lowerBound(segments, matchedColumn, min); + const max = upperBound(segments, matchedColumn, min); + + const result = []; + for (; min <= max; min++) { + const segment = segments[min]; + result.push(GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN])); + } + return result; +} + +function generatedPosition( + map: TraceMap, + source: string, + line: number, + column: number, + bias: Bias, + all: false, +): GeneratedMapping | InvalidGeneratedMapping; +function generatedPosition( + map: TraceMap, + source: string, + line: number, + column: number, + bias: Bias, + all: true, +): GeneratedMapping[]; +function generatedPosition( + map: TraceMap, + source: string, + line: number, + column: number, + bias: Bias, + all: boolean, +): GeneratedMapping | InvalidGeneratedMapping | GeneratedMapping[] { + line--; + if (line < 0) throw new Error(LINE_GTR_ZERO); + if (column < 0) throw new Error(COL_GTR_EQ_ZERO); + + const { sources, resolvedSources } = map; + let sourceIndex = sources.indexOf(source); + if (sourceIndex === -1) sourceIndex = resolvedSources.indexOf(source); + if (sourceIndex === -1) return all ? [] : GMapping(null, null); + + const bySourceMemos = (cast(map)._bySourceMemos ||= sources.map(memoizedState)); + const generated = (cast(map)._bySources ||= buildBySources(decodedMappings(map), bySourceMemos)); + + const segments = generated[sourceIndex][line]; + if (segments == null) return all ? [] : GMapping(null, null); + + const memo = bySourceMemos[sourceIndex]; + + if (all) return sliceGeneratedPositions(segments, memo, line, column, bias); + + const index = traceSegmentInternal(segments, memo, line, column, bias); + if (index === -1) return GMapping(null, null); + + const segment = segments[index]; + return GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/types.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/types.ts new file mode 100644 index 00000000..730a61fb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/types.ts @@ -0,0 +1,114 @@ +import type { SourceMapSegment } from './sourcemap-segment'; +import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping'; + +export interface SourceMapV3 { + file?: string | null; + names: string[]; + sourceRoot?: string; + sources: (string | null)[]; + sourcesContent?: (string | null)[]; + version: 3; + ignoreList?: number[]; +} + +export interface EncodedSourceMap extends SourceMapV3 { + mappings: string; +} + +export interface DecodedSourceMap extends SourceMapV3 { + mappings: SourceMapSegment[][]; +} + +export interface Section { + offset: { line: number; column: number }; + map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap; +} + +export interface SectionedSourceMap { + file?: string | null; + sections: Section[]; + version: 3; +} + +export type OriginalMapping = { + source: string | null; + line: number; + column: number; + name: string | null; +}; + +export type InvalidOriginalMapping = { + source: null; + line: null; + column: null; + name: null; +}; + +export type GeneratedMapping = { + line: number; + column: number; +}; +export type InvalidGeneratedMapping = { + line: null; + column: null; +}; + +export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND; + +export type XInput = { x_google_ignoreList?: SourceMapV3['ignoreList'] }; +export type EncodedSourceMapXInput = EncodedSourceMap & XInput; +export type DecodedSourceMapXInput = DecodedSourceMap & XInput; +export type SectionedSourceMapXInput = Omit & { + sections: SectionXInput[]; +}; +export type SectionXInput = Omit & { + map: SectionedSourceMapInput; +}; + +export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap; +export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput; + +export type Needle = { line: number; column: number; bias?: Bias }; +export type SourceNeedle = { source: string; line: number; column: number; bias?: Bias }; + +export type EachMapping = + | { + generatedLine: number; + generatedColumn: number; + source: null; + originalLine: null; + originalColumn: null; + name: null; + } + | { + generatedLine: number; + generatedColumn: number; + source: string | null; + originalLine: number; + originalColumn: number; + name: string | null; + }; + +export abstract class SourceMap { + declare version: SourceMapV3['version']; + declare file: SourceMapV3['file']; + declare names: SourceMapV3['names']; + declare sourceRoot: SourceMapV3['sourceRoot']; + declare sources: SourceMapV3['sources']; + declare sourcesContent: SourceMapV3['sourcesContent']; + declare resolvedSources: SourceMapV3['sources']; + declare ignoreList: SourceMapV3['ignoreList']; +} + +export type Ro = + T extends Array + ? V[] | Readonly | RoArray | Readonly> + : T extends object + ? T | Readonly | RoObject | Readonly> + : T; +type RoArray = Ro[]; +type RoObject = { [K in keyof T]: T[K] | Ro }; + +export function parse(map: T): Exclude { + return typeof map === 'string' ? JSON.parse(map) : (map as Exclude); +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts new file mode 100644 index 00000000..b7bb85c9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts @@ -0,0 +1,33 @@ +import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment.cts'; +export type MemoState = { + lastKey: number; + lastNeedle: number; + lastIndex: number; +}; +export declare let found: boolean; +/** + * A binary search implementation that returns the index if a match is found. + * If no match is found, then the left-index (the index associated with the item that comes just + * before the desired index) is returned. To maintain proper sort order, a splice would happen at + * the next index: + * + * ```js + * const array = [1, 3]; + * const needle = 2; + * const index = binarySearch(array, needle, (item, needle) => item - needle); + * + * assert.equal(index, 0); + * array.splice(index + 1, 0, needle); + * assert.deepEqual(array, [1, 2, 3]); + * ``` + */ +export declare function binarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, low: number, high: number): number; +export declare function upperBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; +export declare function lowerBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; +export declare function memoizedState(): MemoState; +/** + * This overly complicated beast is just to record the last tested line/column and the resulting + * index, allowing us to skip a few tests if mappings are monotonically increasing. + */ +export declare function memoizedBinarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, state: MemoState, key: number): number; +//# sourceMappingURL=binary-search.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts.map new file mode 100644 index 00000000..648e84c1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"binary-search.d.ts","sourceRoot":"","sources":["../src/binary-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,IAAI,KAAK,SAAQ,CAAC;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CAmBR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,aAAa,IAAI,SAAS,CAMzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACV,MAAM,CAsBR"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts new file mode 100644 index 00000000..19e1e6b9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts @@ -0,0 +1,33 @@ +import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment.mts'; +export type MemoState = { + lastKey: number; + lastNeedle: number; + lastIndex: number; +}; +export declare let found: boolean; +/** + * A binary search implementation that returns the index if a match is found. + * If no match is found, then the left-index (the index associated with the item that comes just + * before the desired index) is returned. To maintain proper sort order, a splice would happen at + * the next index: + * + * ```js + * const array = [1, 3]; + * const needle = 2; + * const index = binarySearch(array, needle, (item, needle) => item - needle); + * + * assert.equal(index, 0); + * array.splice(index + 1, 0, needle); + * assert.deepEqual(array, [1, 2, 3]); + * ``` + */ +export declare function binarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, low: number, high: number): number; +export declare function upperBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; +export declare function lowerBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number; +export declare function memoizedState(): MemoState; +/** + * This overly complicated beast is just to record the last tested line/column and the resulting + * index, allowing us to skip a few tests if mappings are monotonically increasing. + */ +export declare function memoizedBinarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, state: MemoState, key: number): number; +//# sourceMappingURL=binary-search.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts.map new file mode 100644 index 00000000..648e84c1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"binary-search.d.ts","sourceRoot":"","sources":["../src/binary-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,IAAI,KAAK,SAAQ,CAAC;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CAmBR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,aAAa,IAAI,SAAS,CAMzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACV,MAAM,CAsBR"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts new file mode 100644 index 00000000..da496939 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts @@ -0,0 +1,4 @@ +import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.cts'; +export type Source = ReverseSegment[][]; +export = function buildBySources(decoded: readonly SourceMapSegment[][], memos: unknown[]): Source[]; +//# sourceMappingURL=by-source.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts.map new file mode 100644 index 00000000..32d2a7a1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"by-source.d.ts","sourceRoot":"","sources":["../src/by-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,cAAc,EAAE,EAAE,CAAC;AAIxC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,EACtC,KAAK,EAAE,OAAO,EAAE,GACf,MAAM,EAAE,CA4BV"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts new file mode 100644 index 00000000..f3610495 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts @@ -0,0 +1,4 @@ +import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.mts'; +export type Source = ReverseSegment[][]; +export default function buildBySources(decoded: readonly SourceMapSegment[][], memos: unknown[]): Source[]; +//# sourceMappingURL=by-source.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts.map new file mode 100644 index 00000000..32d2a7a1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"by-source.d.ts","sourceRoot":"","sources":["../src/by-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,cAAc,EAAE,EAAE,CAAC;AAIxC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,EACtC,KAAK,EAAE,OAAO,EAAE,GACf,MAAM,EAAE,CA4BV"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts new file mode 100644 index 00000000..433d849b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts @@ -0,0 +1,9 @@ +import { TraceMap } from './trace-mapping.cts'; +import type { SectionedSourceMapInput, Ro } from './types.cts'; +type FlattenMap = { + new (map: Ro, mapUrl?: string | null): TraceMap; + (map: Ro, mapUrl?: string | null): TraceMap; +}; +export declare const FlattenMap: FlattenMap; +export {}; +//# sourceMappingURL=flatten-map.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts.map new file mode 100644 index 00000000..994b208a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"flatten-map.d.ts","sourceRoot":"","sources":["../src/flatten-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwC,MAAM,iBAAiB,CAAC;AAUjF,OAAO,KAAK,EAKV,uBAAuB,EAEvB,EAAE,EACH,MAAM,SAAS,CAAC;AAGjB,KAAK,UAAU,GAAG;IAChB,KAAK,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;IACzE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;CACtE,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,UAsCV,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts new file mode 100644 index 00000000..444a1bed --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts @@ -0,0 +1,9 @@ +import { TraceMap } from './trace-mapping.mts'; +import type { SectionedSourceMapInput, Ro } from './types.mts'; +type FlattenMap = { + new (map: Ro, mapUrl?: string | null): TraceMap; + (map: Ro, mapUrl?: string | null): TraceMap; +}; +export declare const FlattenMap: FlattenMap; +export {}; +//# sourceMappingURL=flatten-map.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts.map new file mode 100644 index 00000000..994b208a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"flatten-map.d.ts","sourceRoot":"","sources":["../src/flatten-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwC,MAAM,iBAAiB,CAAC;AAUjF,OAAO,KAAK,EAKV,uBAAuB,EAEvB,EAAE,EACH,MAAM,SAAS,CAAC;AAGjB,KAAK,UAAU,GAAG;IAChB,KAAK,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;IACzE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;CACtE,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,UAsCV,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts new file mode 100644 index 00000000..62aeedb5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts @@ -0,0 +1,4 @@ +type Resolve = (source: string | null) => string; +export = function resolver(mapUrl: string | null | undefined, sourceRoot: string | undefined): Resolve; +export {}; +//# sourceMappingURL=resolve.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts.map new file mode 100644 index 00000000..9f155ace --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,KAAK,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;AACjD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAQT"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts new file mode 100644 index 00000000..e2798a19 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts @@ -0,0 +1,4 @@ +type Resolve = (source: string | null) => string; +export default function resolver(mapUrl: string | null | undefined, sourceRoot: string | undefined): Resolve; +export {}; +//# sourceMappingURL=resolve.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts.map new file mode 100644 index 00000000..9f155ace --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,KAAK,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;AACjD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAQT"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts new file mode 100644 index 00000000..aa14c129 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts @@ -0,0 +1,4 @@ +import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.cts'; +export = function maybeSort(mappings: SourceMapSegment[][], owned: boolean): SourceMapSegment[][]; +export declare function sortComparator(a: T, b: T): number; +//# sourceMappingURL=sort.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts.map new file mode 100644 index 00000000..48b8e674 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"sort.d.ts","sourceRoot":"","sources":["../src/sort.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAC9B,KAAK,EAAE,OAAO,GACb,gBAAgB,EAAE,EAAE,CAYtB;AAuBD,wBAAgB,cAAc,CAAC,CAAC,SAAS,gBAAgB,GAAG,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAE9F"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts new file mode 100644 index 00000000..c5b94e64 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts @@ -0,0 +1,4 @@ +import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.mts'; +export default function maybeSort(mappings: SourceMapSegment[][], owned: boolean): SourceMapSegment[][]; +export declare function sortComparator(a: T, b: T): number; +//# sourceMappingURL=sort.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts.map new file mode 100644 index 00000000..48b8e674 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"sort.d.ts","sourceRoot":"","sources":["../src/sort.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAC9B,KAAK,EAAE,OAAO,GACb,gBAAgB,EAAE,EAAE,CAYtB;AAuBD,wBAAgB,cAAc,CAAC,CAAC,SAAS,gBAAgB,GAAG,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAE9F"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts new file mode 100644 index 00000000..8d3cabc1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts @@ -0,0 +1,17 @@ +type GeneratedColumn = number; +type SourcesIndex = number; +type SourceLine = number; +type SourceColumn = number; +type NamesIndex = number; +type GeneratedLine = number; +export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; +export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn]; +export declare const COLUMN = 0; +export declare const SOURCES_INDEX = 1; +export declare const SOURCE_LINE = 2; +export declare const SOURCE_COLUMN = 3; +export declare const NAMES_INDEX = 4; +export declare const REV_GENERATED_LINE = 1; +export declare const REV_GENERATED_COLUMN = 2; +export {}; +//# sourceMappingURL=sourcemap-segment.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts.map new file mode 100644 index 00000000..0c94a461 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC;AAE5E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts new file mode 100644 index 00000000..8d3cabc1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts @@ -0,0 +1,17 @@ +type GeneratedColumn = number; +type SourcesIndex = number; +type SourceLine = number; +type SourceColumn = number; +type NamesIndex = number; +type GeneratedLine = number; +export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex]; +export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn]; +export declare const COLUMN = 0; +export declare const SOURCES_INDEX = 1; +export declare const SOURCE_LINE = 2; +export declare const SOURCE_COLUMN = 3; +export declare const NAMES_INDEX = 4; +export declare const REV_GENERATED_LINE = 1; +export declare const REV_GENERATED_COLUMN = 2; +export {}; +//# sourceMappingURL=sourcemap-segment.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts.map new file mode 100644 index 00000000..0c94a461 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC;AAE5E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts new file mode 100644 index 00000000..8b3c0e9b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts @@ -0,0 +1,5 @@ +/** + * Removes everything after the last "/", but leaves the slash. + */ +export = function stripFilename(path: string | undefined | null): string; +//# sourceMappingURL=strip-filename.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts.map new file mode 100644 index 00000000..17a25da0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"strip-filename.d.ts","sourceRoot":"","sources":["../src/strip-filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAI7E"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts new file mode 100644 index 00000000..cbbaee0d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts @@ -0,0 +1,5 @@ +/** + * Removes everything after the last "/", but leaves the slash. + */ +export default function stripFilename(path: string | undefined | null): string; +//# sourceMappingURL=strip-filename.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts.map new file mode 100644 index 00000000..17a25da0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"strip-filename.d.ts","sourceRoot":"","sources":["../src/strip-filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAI7E"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts new file mode 100644 index 00000000..a40f3054 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts @@ -0,0 +1,80 @@ +import type { SourceMapSegment } from './sourcemap-segment.cts'; +import type { SourceMapV3, DecodedSourceMap, EncodedSourceMap, InvalidOriginalMapping, OriginalMapping, InvalidGeneratedMapping, GeneratedMapping, SourceMapInput, Needle, SourceNeedle, SourceMap, EachMapping, Ro } from './types.cts'; +export type { SourceMapSegment } from './sourcemap-segment.cts'; +export type { SourceMap, DecodedSourceMap, EncodedSourceMap, Section, SectionedSourceMap, SourceMapV3, Bias, EachMapping, GeneratedMapping, InvalidGeneratedMapping, InvalidOriginalMapping, Needle, OriginalMapping, OriginalMapping as Mapping, SectionedSourceMapInput, SourceMapInput, SourceNeedle, XInput, EncodedSourceMapXInput, DecodedSourceMapXInput, SectionedSourceMapXInput, SectionXInput, } from './types.cts'; +export declare const LEAST_UPPER_BOUND = -1; +export declare const GREATEST_LOWER_BOUND = 1; +export { FlattenMap, FlattenMap as AnyMap } from './flatten-map.cts'; +export declare class TraceMap implements SourceMap { + version: SourceMapV3['version']; + file: SourceMapV3['file']; + names: SourceMapV3['names']; + sourceRoot: SourceMapV3['sourceRoot']; + sources: SourceMapV3['sources']; + sourcesContent: SourceMapV3['sourcesContent']; + ignoreList: SourceMapV3['ignoreList']; + resolvedSources: string[]; + private _encoded; + private _decoded; + private _decodedMemo; + private _bySources; + private _bySourceMemos; + constructor(map: Ro, mapUrl?: string | null); +} +/** + * Returns the encoded (VLQ string) form of the SourceMap's mappings field. + */ +export declare function encodedMappings(map: TraceMap): EncodedSourceMap['mappings']; +/** + * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field. + */ +export declare function decodedMappings(map: TraceMap): Readonly; +/** + * A low-level API to find the segment associated with a generated line/column (think, from a + * stack trace). Line and column here are 0-based, unlike `originalPositionFor`. + */ +export declare function traceSegment(map: TraceMap, line: number, column: number): Readonly | null; +/** + * A higher-level API to find the source/line/column associated with a generated line/column + * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in + * `source-map` library. + */ +export declare function originalPositionFor(map: TraceMap, needle: Needle): OriginalMapping | InvalidOriginalMapping; +/** + * Finds the generated line/column position of the provided source/line/column source position. + */ +export declare function generatedPositionFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping | InvalidGeneratedMapping; +/** + * Finds all generated line/column positions of the provided source/line/column source position. + */ +export declare function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[]; +/** + * Iterates each mapping in generated position order. + */ +export declare function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void; +/** + * Retrieves the source content for a particular source, if its found. Returns null if not. + */ +export declare function sourceContentFor(map: TraceMap, source: string): string | null; +/** + * Determines if the source is marked to ignore by the source map. + */ +export declare function isIgnored(map: TraceMap, source: string): boolean; +/** + * A helper that skips sorting of the input map's mappings array, which can be expensive for larger + * maps. + */ +export declare function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap; +/** + * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function decodedMap(map: TraceMap): Omit & { + mappings: readonly SourceMapSegment[][]; +}; +/** + * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function encodedMap(map: TraceMap): EncodedSourceMap; +//# sourceMappingURL=trace-mapping.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts.map new file mode 100644 index 00000000..b5a874c0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"trace-mapping.d.ts","sourceRoot":"","sources":["../src/trace-mapping.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,EACT,WAAW,EAIX,EAAE,EACH,MAAM,SAAS,CAAC;AAIjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,WAAW,EACX,IAAI,EACJ,WAAW,EACX,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,MAAM,EACN,eAAe,EACf,eAAe,IAAI,OAAO,EAC1B,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,MAAM,EACN,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,aAAa,GACd,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,eAAe,CAAC;AAEjE,qBAAa,QAAS,YAAW,SAAS;IAChC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAEtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAClC,QAAgB,QAAQ,CAAqB;IAE7C,QAAgB,QAAQ,CAAmC;IAC3D,QAAgB,YAAY,CAAY;IAExC,QAAgB,UAAU,CAAuB;IACjD,QAAgB,cAAc,CAA0B;gBAE5C,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAmC5D;AAUD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAE3E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAErF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAiBnC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,GACb,eAAe,GAAG,sBAAsB,CAiC1C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,YAAY,GACnB,gBAAgB,GAAG,uBAAuB,CAG5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAIhG;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAgCnF;AASD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAIpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,QAAQ,GACZ,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IAAE,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAA;CAAE,CAElF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAE1D"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts new file mode 100644 index 00000000..bc2ff0f1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts @@ -0,0 +1,80 @@ +import type { SourceMapSegment } from './sourcemap-segment.mts'; +import type { SourceMapV3, DecodedSourceMap, EncodedSourceMap, InvalidOriginalMapping, OriginalMapping, InvalidGeneratedMapping, GeneratedMapping, SourceMapInput, Needle, SourceNeedle, SourceMap, EachMapping, Ro } from './types.mts'; +export type { SourceMapSegment } from './sourcemap-segment.mts'; +export type { SourceMap, DecodedSourceMap, EncodedSourceMap, Section, SectionedSourceMap, SourceMapV3, Bias, EachMapping, GeneratedMapping, InvalidGeneratedMapping, InvalidOriginalMapping, Needle, OriginalMapping, OriginalMapping as Mapping, SectionedSourceMapInput, SourceMapInput, SourceNeedle, XInput, EncodedSourceMapXInput, DecodedSourceMapXInput, SectionedSourceMapXInput, SectionXInput, } from './types.mts'; +export declare const LEAST_UPPER_BOUND = -1; +export declare const GREATEST_LOWER_BOUND = 1; +export { FlattenMap, FlattenMap as AnyMap } from './flatten-map.mts'; +export declare class TraceMap implements SourceMap { + version: SourceMapV3['version']; + file: SourceMapV3['file']; + names: SourceMapV3['names']; + sourceRoot: SourceMapV3['sourceRoot']; + sources: SourceMapV3['sources']; + sourcesContent: SourceMapV3['sourcesContent']; + ignoreList: SourceMapV3['ignoreList']; + resolvedSources: string[]; + private _encoded; + private _decoded; + private _decodedMemo; + private _bySources; + private _bySourceMemos; + constructor(map: Ro, mapUrl?: string | null); +} +/** + * Returns the encoded (VLQ string) form of the SourceMap's mappings field. + */ +export declare function encodedMappings(map: TraceMap): EncodedSourceMap['mappings']; +/** + * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field. + */ +export declare function decodedMappings(map: TraceMap): Readonly; +/** + * A low-level API to find the segment associated with a generated line/column (think, from a + * stack trace). Line and column here are 0-based, unlike `originalPositionFor`. + */ +export declare function traceSegment(map: TraceMap, line: number, column: number): Readonly | null; +/** + * A higher-level API to find the source/line/column associated with a generated line/column + * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in + * `source-map` library. + */ +export declare function originalPositionFor(map: TraceMap, needle: Needle): OriginalMapping | InvalidOriginalMapping; +/** + * Finds the generated line/column position of the provided source/line/column source position. + */ +export declare function generatedPositionFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping | InvalidGeneratedMapping; +/** + * Finds all generated line/column positions of the provided source/line/column source position. + */ +export declare function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[]; +/** + * Iterates each mapping in generated position order. + */ +export declare function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void; +/** + * Retrieves the source content for a particular source, if its found. Returns null if not. + */ +export declare function sourceContentFor(map: TraceMap, source: string): string | null; +/** + * Determines if the source is marked to ignore by the source map. + */ +export declare function isIgnored(map: TraceMap, source: string): boolean; +/** + * A helper that skips sorting of the input map's mappings array, which can be expensive for larger + * maps. + */ +export declare function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap; +/** + * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function decodedMap(map: TraceMap): Omit & { + mappings: readonly SourceMapSegment[][]; +}; +/** + * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects + * a sourcemap, or to JSON.stringify. + */ +export declare function encodedMap(map: TraceMap): EncodedSourceMap; +//# sourceMappingURL=trace-mapping.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts.map new file mode 100644 index 00000000..b5a874c0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"trace-mapping.d.ts","sourceRoot":"","sources":["../src/trace-mapping.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,EACT,WAAW,EAIX,EAAE,EACH,MAAM,SAAS,CAAC;AAIjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,WAAW,EACX,IAAI,EACJ,WAAW,EACX,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,MAAM,EACN,eAAe,EACf,eAAe,IAAI,OAAO,EAC1B,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,MAAM,EACN,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,aAAa,GACd,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,eAAe,CAAC;AAEjE,qBAAa,QAAS,YAAW,SAAS;IAChC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAEtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAClC,QAAgB,QAAQ,CAAqB;IAE7C,QAAgB,QAAQ,CAAmC;IAC3D,QAAgB,YAAY,CAAY;IAExC,QAAgB,UAAU,CAAuB;IACjD,QAAgB,cAAc,CAA0B;gBAE5C,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAmC5D;AAUD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAE3E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAErF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAiBnC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,GACb,eAAe,GAAG,sBAAsB,CAiC1C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,YAAY,GACnB,gBAAgB,GAAG,uBAAuB,CAG5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAIhG;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAgCnF;AASD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAIpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,QAAQ,GACZ,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IAAE,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAA;CAAE,CAElF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAE1D"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts new file mode 100644 index 00000000..729c2c32 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts @@ -0,0 +1,107 @@ +import type { SourceMapSegment } from './sourcemap-segment.cts'; +import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping.cts'; +export interface SourceMapV3 { + file?: string | null; + names: string[]; + sourceRoot?: string; + sources: (string | null)[]; + sourcesContent?: (string | null)[]; + version: 3; + ignoreList?: number[]; +} +export interface EncodedSourceMap extends SourceMapV3 { + mappings: string; +} +export interface DecodedSourceMap extends SourceMapV3 { + mappings: SourceMapSegment[][]; +} +export interface Section { + offset: { + line: number; + column: number; + }; + map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap; +} +export interface SectionedSourceMap { + file?: string | null; + sections: Section[]; + version: 3; +} +export type OriginalMapping = { + source: string | null; + line: number; + column: number; + name: string | null; +}; +export type InvalidOriginalMapping = { + source: null; + line: null; + column: null; + name: null; +}; +export type GeneratedMapping = { + line: number; + column: number; +}; +export type InvalidGeneratedMapping = { + line: null; + column: null; +}; +export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND; +export type XInput = { + x_google_ignoreList?: SourceMapV3['ignoreList']; +}; +export type EncodedSourceMapXInput = EncodedSourceMap & XInput; +export type DecodedSourceMapXInput = DecodedSourceMap & XInput; +export type SectionedSourceMapXInput = Omit & { + sections: SectionXInput[]; +}; +export type SectionXInput = Omit & { + map: SectionedSourceMapInput; +}; +export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap; +export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput; +export type Needle = { + line: number; + column: number; + bias?: Bias; +}; +export type SourceNeedle = { + source: string; + line: number; + column: number; + bias?: Bias; +}; +export type EachMapping = { + generatedLine: number; + generatedColumn: number; + source: null; + originalLine: null; + originalColumn: null; + name: null; +} | { + generatedLine: number; + generatedColumn: number; + source: string | null; + originalLine: number; + originalColumn: number; + name: string | null; +}; +export declare abstract class SourceMap { + version: SourceMapV3['version']; + file: SourceMapV3['file']; + names: SourceMapV3['names']; + sourceRoot: SourceMapV3['sourceRoot']; + sources: SourceMapV3['sources']; + sourcesContent: SourceMapV3['sourcesContent']; + resolvedSources: SourceMapV3['sources']; + ignoreList: SourceMapV3['ignoreList']; +} +export type Ro = T extends Array ? V[] | Readonly | RoArray | Readonly> : T extends object ? T | Readonly | RoObject | Readonly> : T; +type RoArray = Ro[]; +type RoObject = { + [K in keyof T]: T[K] | Ro; +}; +export declare function parse(map: T): Exclude; +export {}; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts.map new file mode 100644 index 00000000..92247839 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,gBAAgB,EAAE,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;CAC/D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG,OAAO,oBAAoB,GAAG,OAAO,iBAAiB,CAAC;AAE1E,MAAM,MAAM,MAAM,GAAG;IAAE,mBAAmB,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG;IAC5E,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AACF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG;IACjD,GAAG,EAAE,uBAAuB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,QAAQ,CAAC;AACjG,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAEhF,MAAM,MAAM,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzF,MAAM,MAAM,WAAW,GACnB;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,IAAI,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;CACZ,GACD;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEN,8BAAsB,SAAS;IACrB,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACxC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,EAAE,CAAC,CAAC,IACd,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACvD,CAAC,SAAS,MAAM,GACd,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACrD,CAAC,CAAC;AACV,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEvD,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAEnD"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts new file mode 100644 index 00000000..a26d1866 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts @@ -0,0 +1,107 @@ +import type { SourceMapSegment } from './sourcemap-segment.mts'; +import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping.mts'; +export interface SourceMapV3 { + file?: string | null; + names: string[]; + sourceRoot?: string; + sources: (string | null)[]; + sourcesContent?: (string | null)[]; + version: 3; + ignoreList?: number[]; +} +export interface EncodedSourceMap extends SourceMapV3 { + mappings: string; +} +export interface DecodedSourceMap extends SourceMapV3 { + mappings: SourceMapSegment[][]; +} +export interface Section { + offset: { + line: number; + column: number; + }; + map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap; +} +export interface SectionedSourceMap { + file?: string | null; + sections: Section[]; + version: 3; +} +export type OriginalMapping = { + source: string | null; + line: number; + column: number; + name: string | null; +}; +export type InvalidOriginalMapping = { + source: null; + line: null; + column: null; + name: null; +}; +export type GeneratedMapping = { + line: number; + column: number; +}; +export type InvalidGeneratedMapping = { + line: null; + column: null; +}; +export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND; +export type XInput = { + x_google_ignoreList?: SourceMapV3['ignoreList']; +}; +export type EncodedSourceMapXInput = EncodedSourceMap & XInput; +export type DecodedSourceMapXInput = DecodedSourceMap & XInput; +export type SectionedSourceMapXInput = Omit & { + sections: SectionXInput[]; +}; +export type SectionXInput = Omit & { + map: SectionedSourceMapInput; +}; +export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap; +export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput; +export type Needle = { + line: number; + column: number; + bias?: Bias; +}; +export type SourceNeedle = { + source: string; + line: number; + column: number; + bias?: Bias; +}; +export type EachMapping = { + generatedLine: number; + generatedColumn: number; + source: null; + originalLine: null; + originalColumn: null; + name: null; +} | { + generatedLine: number; + generatedColumn: number; + source: string | null; + originalLine: number; + originalColumn: number; + name: string | null; +}; +export declare abstract class SourceMap { + version: SourceMapV3['version']; + file: SourceMapV3['file']; + names: SourceMapV3['names']; + sourceRoot: SourceMapV3['sourceRoot']; + sources: SourceMapV3['sources']; + sourcesContent: SourceMapV3['sourcesContent']; + resolvedSources: SourceMapV3['sources']; + ignoreList: SourceMapV3['ignoreList']; +} +export type Ro = T extends Array ? V[] | Readonly | RoArray | Readonly> : T extends object ? T | Readonly | RoObject | Readonly> : T; +type RoArray = Ro[]; +type RoObject = { + [K in keyof T]: T[K] | Ro; +}; +export declare function parse(map: T): Exclude; +export {}; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts.map new file mode 100644 index 00000000..92247839 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,gBAAgB,EAAE,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;CAC/D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG,OAAO,oBAAoB,GAAG,OAAO,iBAAiB,CAAC;AAE1E,MAAM,MAAM,MAAM,GAAG;IAAE,mBAAmB,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG;IAC5E,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AACF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG;IACjD,GAAG,EAAE,uBAAuB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,QAAQ,CAAC;AACjG,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAEhF,MAAM,MAAM,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzF,MAAM,MAAM,WAAW,GACnB;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,IAAI,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;CACZ,GACD;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEN,8BAAsB,SAAS;IACrB,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACxC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,EAAE,CAAC,CAAC,IACd,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACvD,CAAC,SAAS,MAAM,GACd,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACrD,CAAC,CAAC;AACV,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEvD,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAEnD"} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/LICENSE new file mode 100644 index 00000000..3d484354 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Anthropic, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/README.md new file mode 100644 index 00000000..7a553ebb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/README.md @@ -0,0 +1,170 @@ +# MCP TypeScript SDK [![NPM Version](https://img.shields.io/npm/v/%40modelcontextprotocol%2Fsdk)](https://www.npmjs.com/package/@modelcontextprotocol/sdk) [![MIT licensed](https://img.shields.io/npm/l/%40modelcontextprotocol%2Fsdk)](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/LICENSE) + +
+Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Core Concepts](#core-concepts) +- [Examples](#examples) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [License](#license) + +
+ +## Overview + +The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements +[the full MCP specification](https://modelcontextprotocol.io/specification/draft), making it easy to: + +- Create MCP servers that expose resources, prompts and tools +- Build MCP clients that can connect to any MCP server +- Use standard transports like stdio and Streamable HTTP + +## Installation + +```bash +npm install @modelcontextprotocol/sdk zod +``` + +This SDK has a **required peer dependency** on `zod` for schema validation. The SDK internally imports from `zod/v4`, but maintains backwards compatibility with projects using Zod v3.25 or later. You can use either API in your code by importing from `zod/v3` or `zod/v4`: + +## Quick Start + +To see the SDK in action end-to-end, start from the runnable examples in `src/examples`: + +1. **Install dependencies** (from the SDK repo root): + + ```bash + npm install + ``` + +2. **Run the example Streamable HTTP server**: + + ```bash + npx tsx src/examples/server/simpleStreamableHttp.ts + ``` + +3. **Run the interactive client in another terminal**: + + ```bash + npx tsx src/examples/client/simpleStreamableHttp.ts + ``` + +This pair of examples demonstrates tools, resources, prompts, sampling, elicitation, tasks and logging. For a guided walkthrough and variations (stateless servers, JSON-only responses, SSE compatibility, OAuth, etc.), see [docs/server.md](docs/server.md) and +[docs/client.md](docs/client.md). + +## Core Concepts + +### Servers and transports + +An MCP server is typically created with `McpServer` and connected to a transport such as Streamable HTTP or stdio. The SDK supports: + +- **Streamable HTTP** for remote servers (recommended). +- **HTTP + SSE** for backwards compatibility only. +- **stdio** for local, process-spawned integrations. + +Runnable server examples live under `src/examples/server` and are documented in [docs/server.md](docs/server.md). + +### Tools, resources, prompts + +- **Tools** let LLMs ask your server to take actions (computation, side effects, network calls). +- **Resources** expose read-only data that clients can surface to users or models. +- **Prompts** are reusable templates that help users talk to models in a consistent way. + +The detailed APIs, including `ResourceTemplate`, completions, and display-name metadata, are covered in [docs/server.md](docs/server.md#tools-resources-and-prompts), with runnable implementations in [`simpleStreamableHttp.ts`](src/examples/server/simpleStreamableHttp.ts). + +### Capabilities: sampling, elicitation, and tasks + +The SDK includes higher-level capabilities for richer workflows: + +- **Sampling**: server-side tools can ask connected clients to run LLM completions. +- **Form elicitation**: tools can request non-sensitive input via structured forms. +- **URL elicitation**: servers can ask users to complete secure flows in a browser (e.g., API key entry, payments, OAuth). +- **Tasks (experimental)**: long-running tool calls can be turned into tasks that you poll or resume later. + +Conceptual overviews and links to runnable examples are in: + +- [docs/capabilities.md](docs/capabilities.md) + +Key example servers include: + +- [`toolWithSampleServer.ts`](src/examples/server/toolWithSampleServer.ts) +- [`elicitationFormExample.ts`](src/examples/server/elicitationFormExample.ts) +- [`elicitationUrlExample.ts`](src/examples/server/elicitationUrlExample.ts) + +### Clients + +The high-level `Client` class connects to MCP servers over different transports and exposes helpers like `listTools`, `callTool`, `listResources`, `readResource`, `listPrompts`, and `getPrompt`. + +Runnable clients live under `src/examples/client` and are described in [docs/client.md](docs/client.md), including: + +- Interactive Streamable HTTP client ([`simpleStreamableHttp.ts`](src/examples/client/simpleStreamableHttp.ts)) +- Streamable HTTP client with SSE fallback ([`streamableHttpWithSseFallbackClient.ts`](src/examples/client/streamableHttpWithSseFallbackClient.ts)) +- OAuth-enabled clients and polling/parallel examples + +### Node.js Web Crypto (globalThis.crypto) compatibility + +Some parts of the SDK (for example, JWT-based client authentication in `auth-extensions.ts` via `jose`) rely on the Web Crypto API exposed as `globalThis.crypto`. + +See [docs/faq.md](docs/faq.md) for details on supported Node.js versions and how to polyfill `globalThis.crypto` when running on older Node.js runtimes. + +## Examples + +The SDK ships runnable examples under `src/examples`. Use these tables to find the scenario you care about and jump straight to the corresponding code and docs. + +### Server examples + +| Scenario | Description | Example file(s) | Related docs | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| Streamable HTTP server (stateful) | Feature-rich server with tools, resources, prompts, logging, tasks, sampling, and optional OAuth. | [`simpleStreamableHttp.ts`](src/examples/server/simpleStreamableHttp.ts) | [`server.md`](docs/server.md), [`capabilities.md`](docs/capabilities.md) | +| Streamable HTTP server (stateless) | No session tracking; good for simple API-style servers. | [`simpleStatelessStreamableHttp.ts`](src/examples/server/simpleStatelessStreamableHttp.ts) | [`server.md`](docs/server.md) | +| JSON response mode (no SSE) | Streamable HTTP with JSON responses only and limited notifications. | [`jsonResponseStreamableHttp.ts`](src/examples/server/jsonResponseStreamableHttp.ts) | [`server.md`](docs/server.md) | +| Server notifications over Streamable HTTP | Demonstrates server-initiated notifications using SSE with Streamable HTTP. | [`standaloneSseWithGetStreamableHttp.ts`](src/examples/server/standaloneSseWithGetStreamableHttp.ts) | [`server.md`](docs/server.md) | +| Deprecated HTTP+SSE server | Legacy HTTP+SSE transport for backwards-compatibility testing. | [`simpleSseServer.ts`](src/examples/server/simpleSseServer.ts) | [`server.md`](docs/server.md) | +| Backwards-compatible server (Streamable HTTP + SSE) | Single server that supports both Streamable HTTP and legacy SSE clients. | [`sseAndStreamableHttpCompatibleServer.ts`](src/examples/server/sseAndStreamableHttpCompatibleServer.ts) | [`server.md`](docs/server.md) | +| Form elicitation server | Uses form elicitation to collect non-sensitive user input. | [`elicitationFormExample.ts`](src/examples/server/elicitationFormExample.ts) | [`capabilities.md`](docs/capabilities.md#elicitation) | +| URL elicitation server | Demonstrates URL-mode elicitation in an OAuth-protected server. | [`elicitationUrlExample.ts`](src/examples/server/elicitationUrlExample.ts) | [`capabilities.md`](docs/capabilities.md#elicitation) | +| Sampling and tasks server | Combines tools, logging, sampling, and experimental task-based execution. | [`toolWithSampleServer.ts`](src/examples/server/toolWithSampleServer.ts) | [`capabilities.md`](docs/capabilities.md) | +| OAuth demo authorization server | In-memory OAuth provider used with the example servers. | [`demoInMemoryOAuthProvider.ts`](src/examples/server/demoInMemoryOAuthProvider.ts) | [`server.md`](docs/server.md) | + +### Client examples + +| Scenario | Description | Example file(s) | Related docs | +| --------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| Interactive Streamable HTTP client | CLI client that exercises tools, resources, prompts, elicitation, and tasks. | [`simpleStreamableHttp.ts`](src/examples/client/simpleStreamableHttp.ts) | [`client.md`](docs/client.md) | +| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, then falls back to SSE on 4xx responses. | [`streamableHttpWithSseFallbackClient.ts`](src/examples/client/streamableHttpWithSseFallbackClient.ts) | [`client.md`](docs/client.md), [`server.md`](docs/server.md) | +| SSE polling client | Polls a legacy SSE server and demonstrates notification handling. | [`ssePollingClient.ts`](src/examples/client/ssePollingClient.ts) | [`client.md`](docs/client.md) | +| Parallel tool calls client | Shows how to run multiple tool calls in parallel. | [`parallelToolCallsClient.ts`](src/examples/client/parallelToolCallsClient.ts) | [`client.md`](docs/client.md) | +| Multiple clients in parallel | Demonstrates connecting multiple clients concurrently to the same server. | [`multipleClientsParallel.ts`](src/examples/client/multipleClientsParallel.ts) | [`client.md`](docs/client.md) | +| OAuth clients | Examples of client_credentials (basic and private_key_jwt) and reusable providers. | [`simpleOAuthClient.ts`](src/examples/client/simpleOAuthClient.ts), [`simpleOAuthClientProvider.ts`](src/examples/client/simpleOAuthClientProvider.ts), [`simpleClientCredentials.ts`](src/examples/client/simpleClientCredentials.ts) | [`client.md`](docs/client.md) | +| URL elicitation client | Works with the URL elicitation server to drive secure browser flows. | [`elicitationUrlExample.ts`](src/examples/client/elicitationUrlExample.ts) | [`capabilities.md`](docs/capabilities.md#elicitation) | + +Shared utilities: + +- In-memory event store for resumability: [`inMemoryEventStore.ts`](src/examples/shared/inMemoryEventStore.ts) (see [`server.md`](docs/server.md)). + +For more details on how to run these examples (including recommended commands and deployment diagrams), see `src/examples/README.md`. + +## Documentation + +- Local SDK docs: + - [docs/server.md](docs/server.md) – building and running MCP servers, transports, tools/resources/prompts, CORS, DNS rebinding, and multi-node deployment. + - [docs/client.md](docs/client.md) – using the high-level client, transports, backwards compatibility, and OAuth helpers. + - [docs/capabilities.md](docs/capabilities.md) – sampling, elicitation (form and URL), and experimental task-based execution. + - [docs/protocol.md](docs/protocol.md) – protocol features: ping, progress, cancellation, pagination, capability negotiation, and JSON Schema. + - [docs/faq.md](docs/faq.md) – environment and troubleshooting FAQs (including Node.js Web Crypto support). +- External references: + - [Model Context Protocol documentation](https://modelcontextprotocol.io) + - [MCP Specification](https://spec.modelcontextprotocol.io) + - [Example Servers](https://github.com/modelcontextprotocol/servers) + +## Contributing + +Issues and pull requests are welcome on GitHub at . + +## License + +This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/package.json new file mode 100644 index 00000000..5d6c68e2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/package.json @@ -0,0 +1,155 @@ +{ + "name": "@modelcontextprotocol/sdk", + "version": "1.27.1", + "description": "Model Context Protocol implementation for TypeScript", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=18" + }, + "keywords": [ + "modelcontextprotocol", + "mcp" + ], + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + }, + "./client": { + "import": "./dist/esm/client/index.js", + "require": "./dist/cjs/client/index.js" + }, + "./server": { + "import": "./dist/esm/server/index.js", + "require": "./dist/cjs/server/index.js" + }, + "./validation": { + "import": "./dist/esm/validation/index.js", + "require": "./dist/cjs/validation/index.js" + }, + "./validation/ajv": { + "import": "./dist/esm/validation/ajv-provider.js", + "require": "./dist/cjs/validation/ajv-provider.js" + }, + "./validation/cfworker": { + "import": "./dist/esm/validation/cfworker-provider.js", + "require": "./dist/cjs/validation/cfworker-provider.js" + }, + "./experimental": { + "import": "./dist/esm/experimental/index.js", + "require": "./dist/cjs/experimental/index.js" + }, + "./experimental/tasks": { + "import": "./dist/esm/experimental/tasks/index.js", + "require": "./dist/cjs/experimental/tasks/index.js" + }, + "./*": { + "import": "./dist/esm/*", + "require": "./dist/cjs/*" + } + }, + "typesVersions": { + "*": { + "*": [ + "./dist/esm/*" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "fetch:spec-types": "tsx scripts/fetch-spec-types.ts", + "typecheck": "tsgo --noEmit", + "build": "npm run build:esm && npm run build:cjs", + "build:esm": "mkdir -p dist/esm && echo '{\"type\": \"module\"}' > dist/esm/package.json && tsc -p tsconfig.prod.json", + "build:esm:w": "npm run build:esm -- -w", + "build:cjs": "mkdir -p dist/cjs && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && tsc -p tsconfig.cjs.json", + "build:cjs:w": "npm run build:cjs -- -w", + "examples:simple-server:w": "tsx --watch src/examples/server/simpleStreamableHttp.ts --oauth", + "prepack": "npm run build:esm && npm run build:cjs", + "lint": "eslint src/ && prettier --check .", + "lint:fix": "eslint src/ --fix && prettier --write .", + "check": "npm run typecheck && npm run lint", + "test": "vitest run", + "test:watch": "vitest", + "start": "npm run server", + "server": "tsx watch --clear-screen=false scripts/cli.ts server", + "client": "tsx scripts/cli.ts client", + "test:conformance:server": "test/conformance/scripts/run-server-conformance.sh --expected-failures test/conformance/conformance-baseline.yml", + "test:conformance:server:all": "test/conformance/scripts/run-server-conformance.sh --suite all --expected-failures test/conformance/conformance-baseline.yml", + "test:conformance:server:run": "npx tsx test/conformance/src/everythingServer.ts", + "test:conformance:client": "npx @modelcontextprotocol/conformance client --command 'npx tsx test/conformance/src/everythingClient.ts' --expected-failures test/conformance/conformance-baseline.yml", + "test:conformance:client:all": "npx @modelcontextprotocol/conformance client --command 'npx tsx test/conformance/src/everythingClient.ts' --suite all --expected-failures test/conformance/conformance-baseline.yml" + }, + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + }, + "devDependencies": { + "@cfworker/json-schema": "^4.1.1", + "@eslint/js": "^9.39.1", + "@modelcontextprotocol/conformance": "^0.1.14", + "@types/content-type": "^1.1.8", + "@types/cors": "^2.8.17", + "@types/cross-spawn": "^6.0.6", + "@types/eventsource": "^1.1.15", + "@types/express": "^5.0.0", + "@types/express-serve-static-core": "^5.1.0", + "@types/node": "^22.12.0", + "@types/supertest": "^6.0.2", + "@types/ws": "^8.5.12", + "@typescript/native-preview": "^7.0.0-dev.20251103.1", + "eslint": "^9.8.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-n": "^17.23.1", + "prettier": "3.6.2", + "supertest": "^7.0.0", + "tsx": "^4.16.5", + "typescript": "^5.5.4", + "typescript-eslint": "^8.48.1", + "vitest": "^4.0.8", + "ws": "^8.18.0" + }, + "resolutions": { + "strip-ansi": "6.0.1" + }, + "overrides": { + "qs": "6.14.1" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/.editorconfig b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/.editorconfig new file mode 100644 index 00000000..b1401639 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Copied from Node.js to ease compatibility in PR. +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +quote_type = single diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/CHANGELOG.md new file mode 100644 index 00000000..2adc7d32 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/CHANGELOG.md @@ -0,0 +1,147 @@ +# Changelog + +## [0.11.0](https://github.com/pkgjs/parseargs/compare/v0.10.0...v0.11.0) (2022-10-08) + + +### Features + +* add `default` option parameter ([#142](https://github.com/pkgjs/parseargs/issues/142)) ([cd20847](https://github.com/pkgjs/parseargs/commit/cd20847a00b2f556aa9c085ac83b942c60868ec1)) + +## [0.10.0](https://github.com/pkgjs/parseargs/compare/v0.9.1...v0.10.0) (2022-07-21) + + +### Features + +* add parsed meta-data to returned properties ([#129](https://github.com/pkgjs/parseargs/issues/129)) ([91bfb4d](https://github.com/pkgjs/parseargs/commit/91bfb4d3f7b6937efab1b27c91c45d1205f1497e)) + +## [0.9.1](https://github.com/pkgjs/parseargs/compare/v0.9.0...v0.9.1) (2022-06-20) + + +### Bug Fixes + +* **runtime:** support node 14+ ([#135](https://github.com/pkgjs/parseargs/issues/135)) ([6a1c5a6](https://github.com/pkgjs/parseargs/commit/6a1c5a6f7cadf2f035e004027e2742e3c4ce554b)) + +## [0.9.0](https://github.com/pkgjs/parseargs/compare/v0.8.0...v0.9.0) (2022-05-23) + + +### ⚠ BREAKING CHANGES + +* drop handling of electron arguments (#121) + +### Code Refactoring + +* drop handling of electron arguments ([#121](https://github.com/pkgjs/parseargs/issues/121)) ([a2ffd53](https://github.com/pkgjs/parseargs/commit/a2ffd537c244a062371522b955acb45a404fc9f2)) + +## [0.8.0](https://github.com/pkgjs/parseargs/compare/v0.7.1...v0.8.0) (2022-05-16) + + +### ⚠ BREAKING CHANGES + +* switch type:string option arguments to greedy, but with error for suspect cases in strict mode (#88) +* positionals now opt-in when strict:true (#116) +* create result.values with null prototype (#111) + +### Features + +* create result.values with null prototype ([#111](https://github.com/pkgjs/parseargs/issues/111)) ([9d539c3](https://github.com/pkgjs/parseargs/commit/9d539c3d57f269c160e74e0656ad4fa84ff92ec2)) +* positionals now opt-in when strict:true ([#116](https://github.com/pkgjs/parseargs/issues/116)) ([3643338](https://github.com/pkgjs/parseargs/commit/364333826b746e8a7dc5505b4b22fd19ac51df3b)) +* switch type:string option arguments to greedy, but with error for suspect cases in strict mode ([#88](https://github.com/pkgjs/parseargs/issues/88)) ([c2b5e72](https://github.com/pkgjs/parseargs/commit/c2b5e72161991dfdc535909f1327cc9b970fe7e8)) + +### [0.7.1](https://github.com/pkgjs/parseargs/compare/v0.7.0...v0.7.1) (2022-04-15) + + +### Bug Fixes + +* resist pollution ([#106](https://github.com/pkgjs/parseargs/issues/106)) ([ecf2dec](https://github.com/pkgjs/parseargs/commit/ecf2dece0a9f2a76d789384d5d71c68ffe64022a)) + +## [0.7.0](https://github.com/pkgjs/parseargs/compare/v0.6.0...v0.7.0) (2022-04-13) + + +### Features + +* Add strict mode to parser ([#74](https://github.com/pkgjs/parseargs/issues/74)) ([8267d02](https://github.com/pkgjs/parseargs/commit/8267d02083a87b8b8a71fcce08348d1e031ea91c)) + +## [0.6.0](https://github.com/pkgjs/parseargs/compare/v0.5.0...v0.6.0) (2022-04-11) + + +### ⚠ BREAKING CHANGES + +* rework results to remove redundant `flags` property and store value true for boolean options (#83) +* switch to existing ERR_INVALID_ARG_VALUE (#97) + +### Code Refactoring + +* rework results to remove redundant `flags` property and store value true for boolean options ([#83](https://github.com/pkgjs/parseargs/issues/83)) ([be153db](https://github.com/pkgjs/parseargs/commit/be153dbed1d488cb7b6e27df92f601ba7337713d)) +* switch to existing ERR_INVALID_ARG_VALUE ([#97](https://github.com/pkgjs/parseargs/issues/97)) ([084a23f](https://github.com/pkgjs/parseargs/commit/084a23f9fde2da030b159edb1c2385f24579ce40)) + +## [0.5.0](https://github.com/pkgjs/parseargs/compare/v0.4.0...v0.5.0) (2022-04-10) + + +### ⚠ BREAKING CHANGES + +* Require type to be specified for each supplied option (#95) + +### Features + +* Require type to be specified for each supplied option ([#95](https://github.com/pkgjs/parseargs/issues/95)) ([02cd018](https://github.com/pkgjs/parseargs/commit/02cd01885b8aaa59f2db8308f2d4479e64340068)) + +## [0.4.0](https://github.com/pkgjs/parseargs/compare/v0.3.0...v0.4.0) (2022-03-12) + + +### ⚠ BREAKING CHANGES + +* parsing, revisit short option groups, add support for combined short and value (#75) +* restructure configuration to take options bag (#63) + +### Code Refactoring + +* parsing, revisit short option groups, add support for combined short and value ([#75](https://github.com/pkgjs/parseargs/issues/75)) ([a92600f](https://github.com/pkgjs/parseargs/commit/a92600fa6c214508ab1e016fa55879a314f541af)) +* restructure configuration to take options bag ([#63](https://github.com/pkgjs/parseargs/issues/63)) ([b412095](https://github.com/pkgjs/parseargs/commit/b4120957d90e809ee8b607b06e747d3e6a6b213e)) + +## [0.3.0](https://github.com/pkgjs/parseargs/compare/v0.2.0...v0.3.0) (2022-02-06) + + +### Features + +* **parser:** support short-option groups ([#59](https://github.com/pkgjs/parseargs/issues/59)) ([882067b](https://github.com/pkgjs/parseargs/commit/882067bc2d7cbc6b796f8e5a079a99bc99d4e6ba)) + +## [0.2.0](https://github.com/pkgjs/parseargs/compare/v0.1.1...v0.2.0) (2022-02-05) + + +### Features + +* basic support for shorts ([#50](https://github.com/pkgjs/parseargs/issues/50)) ([a2f36d7](https://github.com/pkgjs/parseargs/commit/a2f36d7da4145af1c92f76806b7fe2baf6beeceb)) + + +### Bug Fixes + +* always store value for a=b ([#43](https://github.com/pkgjs/parseargs/issues/43)) ([a85e8dc](https://github.com/pkgjs/parseargs/commit/a85e8dc06379fd2696ee195cc625de8fac6aee42)) +* support single dash as positional ([#49](https://github.com/pkgjs/parseargs/issues/49)) ([d795bf8](https://github.com/pkgjs/parseargs/commit/d795bf877d068fd67aec381f30b30b63f97109ad)) + +### [0.1.1](https://github.com/pkgjs/parseargs/compare/v0.1.0...v0.1.1) (2022-01-25) + + +### Bug Fixes + +* only use arrays in results for multiples ([#42](https://github.com/pkgjs/parseargs/issues/42)) ([c357584](https://github.com/pkgjs/parseargs/commit/c357584847912506319ed34a0840080116f4fd65)) + +## 0.1.0 (2022-01-22) + + +### Features + +* expand scenarios covered by default arguments for environments ([#20](https://github.com/pkgjs/parseargs/issues/20)) ([582ada7](https://github.com/pkgjs/parseargs/commit/582ada7be0eca3a73d6e0bd016e7ace43449fa4c)) +* update readme and include contributing guidelines ([8edd6fc](https://github.com/pkgjs/parseargs/commit/8edd6fc863cd705f6fac732724159ebe8065a2b0)) + + +### Bug Fixes + +* do not strip excess leading dashes on long option names ([#21](https://github.com/pkgjs/parseargs/issues/21)) ([f848590](https://github.com/pkgjs/parseargs/commit/f848590ebf3249ed5979ff47e003fa6e1a8ec5c0)) +* name & readme ([3f057c1](https://github.com/pkgjs/parseargs/commit/3f057c1b158a1bdbe878c64b57460c58e56e465f)) +* package.json values ([9bac300](https://github.com/pkgjs/parseargs/commit/9bac300e00cd76c77076bf9e75e44f8929512da9)) +* update readme name ([957d8d9](https://github.com/pkgjs/parseargs/commit/957d8d96e1dcb48297c0a14345d44c0123b2883e)) + + +### Build System + +* first release as minor ([421c6e2](https://github.com/pkgjs/parseargs/commit/421c6e2569a8668ad14fac5a5af5be60479a7571)) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/README.md new file mode 100644 index 00000000..0a041927 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/README.md @@ -0,0 +1,413 @@ + +# parseArgs + +[![Coverage][coverage-image]][coverage-url] + +Polyfill of `util.parseArgs()` + +## `util.parseArgs([config])` + + + +> Stability: 1 - Experimental + +* `config` {Object} Used to provide arguments for parsing and to configure + the parser. `config` supports the following properties: + * `args` {string\[]} array of argument strings. **Default:** `process.argv` + with `execPath` and `filename` removed. + * `options` {Object} Used to describe arguments known to the parser. + Keys of `options` are the long names of options and values are an + {Object} accepting the following properties: + * `type` {string} Type of argument, which must be either `boolean` or `string`. + * `multiple` {boolean} Whether this option can be provided multiple + times. If `true`, all values will be collected in an array. If + `false`, values for the option are last-wins. **Default:** `false`. + * `short` {string} A single character alias for the option. + * `default` {string | boolean | string\[] | boolean\[]} The default option + value when it is not set by args. It must be of the same type as the + the `type` property. When `multiple` is `true`, it must be an array. + * `strict` {boolean} Should an error be thrown when unknown arguments + are encountered, or when arguments are passed that do not match the + `type` configured in `options`. + **Default:** `true`. + * `allowPositionals` {boolean} Whether this command accepts positional + arguments. + **Default:** `false` if `strict` is `true`, otherwise `true`. + * `tokens` {boolean} Return the parsed tokens. This is useful for extending + the built-in behavior, from adding additional checks through to reprocessing + the tokens in different ways. + **Default:** `false`. + +* Returns: {Object} The parsed command line arguments: + * `values` {Object} A mapping of parsed option names with their {string} + or {boolean} values. + * `positionals` {string\[]} Positional arguments. + * `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens) + section. Only returned if `config` includes `tokens: true`. + +Provides a higher level API for command-line argument parsing than interacting +with `process.argv` directly. Takes a specification for the expected arguments +and returns a structured object with the parsed options and positionals. + +```mjs +import { parseArgs } from 'node:util'; +const args = ['-f', '--bar', 'b']; +const options = { + foo: { + type: 'boolean', + short: 'f' + }, + bar: { + type: 'string' + } +}; +const { + values, + positionals +} = parseArgs({ args, options }); +console.log(values, positionals); +// Prints: [Object: null prototype] { foo: true, bar: 'b' } [] +``` + +```cjs +const { parseArgs } = require('node:util'); +const args = ['-f', '--bar', 'b']; +const options = { + foo: { + type: 'boolean', + short: 'f' + }, + bar: { + type: 'string' + } +}; +const { + values, + positionals +} = parseArgs({ args, options }); +console.log(values, positionals); +// Prints: [Object: null prototype] { foo: true, bar: 'b' } [] +``` + +`util.parseArgs` is experimental and behavior may change. Join the +conversation in [pkgjs/parseargs][] to contribute to the design. + +### `parseArgs` `tokens` + +Detailed parse information is available for adding custom behaviours by +specifying `tokens: true` in the configuration. +The returned tokens have properties describing: + +* all tokens + * `kind` {string} One of 'option', 'positional', or 'option-terminator'. + * `index` {number} Index of element in `args` containing token. So the + source argument for a token is `args[token.index]`. +* option tokens + * `name` {string} Long name of option. + * `rawName` {string} How option used in args, like `-f` of `--foo`. + * `value` {string | undefined} Option value specified in args. + Undefined for boolean options. + * `inlineValue` {boolean | undefined} Whether option value specified inline, + like `--foo=bar`. +* positional tokens + * `value` {string} The value of the positional argument in args (i.e. `args[index]`). +* option-terminator token + +The returned tokens are in the order encountered in the input args. Options +that appear more than once in args produce a token for each use. Short option +groups like `-xy` expand to a token for each option. So `-xxx` produces +three tokens. + +For example to use the returned tokens to add support for a negated option +like `--no-color`, the tokens can be reprocessed to change the value stored +for the negated option. + +```mjs +import { parseArgs } from 'node:util'; + +const options = { + 'color': { type: 'boolean' }, + 'no-color': { type: 'boolean' }, + 'logfile': { type: 'string' }, + 'no-logfile': { type: 'boolean' }, +}; +const { values, tokens } = parseArgs({ options, tokens: true }); + +// Reprocess the option tokens and overwrite the returned values. +tokens + .filter((token) => token.kind === 'option') + .forEach((token) => { + if (token.name.startsWith('no-')) { + // Store foo:false for --no-foo + const positiveName = token.name.slice(3); + values[positiveName] = false; + delete values[token.name]; + } else { + // Resave value so last one wins if both --foo and --no-foo. + values[token.name] = token.value ?? true; + } + }); + +const color = values.color; +const logfile = values.logfile ?? 'default.log'; + +console.log({ logfile, color }); +``` + +```cjs +const { parseArgs } = require('node:util'); + +const options = { + 'color': { type: 'boolean' }, + 'no-color': { type: 'boolean' }, + 'logfile': { type: 'string' }, + 'no-logfile': { type: 'boolean' }, +}; +const { values, tokens } = parseArgs({ options, tokens: true }); + +// Reprocess the option tokens and overwrite the returned values. +tokens + .filter((token) => token.kind === 'option') + .forEach((token) => { + if (token.name.startsWith('no-')) { + // Store foo:false for --no-foo + const positiveName = token.name.slice(3); + values[positiveName] = false; + delete values[token.name]; + } else { + // Resave value so last one wins if both --foo and --no-foo. + values[token.name] = token.value ?? true; + } + }); + +const color = values.color; +const logfile = values.logfile ?? 'default.log'; + +console.log({ logfile, color }); +``` + +Example usage showing negated options, and when an option is used +multiple ways then last one wins. + +```console +$ node negate.js +{ logfile: 'default.log', color: undefined } +$ node negate.js --no-logfile --no-color +{ logfile: false, color: false } +$ node negate.js --logfile=test.log --color +{ logfile: 'test.log', color: true } +$ node negate.js --no-logfile --logfile=test.log --color --no-color +{ logfile: 'test.log', color: false } +``` + +----- + + +## Table of Contents +- [`util.parseArgs([config])`](#utilparseargsconfig) +- [Scope](#scope) +- [Version Matchups](#version-matchups) +- [🚀 Getting Started](#-getting-started) +- [🙌 Contributing](#-contributing) +- [💡 `process.mainArgs` Proposal](#-processmainargs-proposal) + - [Implementation:](#implementation) +- [📃 Examples](#-examples) +- [F.A.Qs](#faqs) +- [Links & Resources](#links--resources) + +----- + +## Scope + +It is already possible to build great arg parsing modules on top of what Node.js provides; the prickly API is abstracted away by these modules. Thus, process.parseArgs() is not necessarily intended for library authors; it is intended for developers of simple CLI tools, ad-hoc scripts, deployed Node.js applications, and learning materials. + +It is exceedingly difficult to provide an API which would both be friendly to these Node.js users while being extensible enough for libraries to build upon. We chose to prioritize these use cases because these are currently not well-served by Node.js' API. + +---- + +## Version Matchups + +| Node.js | @pkgjs/parseArgs | +| -- | -- | +| [v18.3.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [v0.9.1](https://github.com/pkgjs/parseargs/tree/v0.9.1#utilparseargsconfig) | +| [v16.17.0](https://nodejs.org/dist/latest-v16.x/docs/api/util.html#utilparseargsconfig), [v18.7.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [0.10.0](https://github.com/pkgjs/parseargs/tree/v0.10.0#utilparseargsconfig) | + +---- + +## 🚀 Getting Started + +1. **Install dependencies.** + + ```bash + npm install + ``` + +2. **Open the index.js file and start editing!** + +3. **Test your code by calling parseArgs through our test file** + + ```bash + npm test + ``` + +---- + +## 🙌 Contributing + +Any person who wants to contribute to the initiative is welcome! Please first read the [Contributing Guide](CONTRIBUTING.md) + +Additionally, reading the [`Examples w/ Output`](#-examples-w-output) section of this document will be the best way to familiarize yourself with the target expected behavior for parseArgs() once it is fully implemented. + +This package was implemented using [tape](https://www.npmjs.com/package/tape) as its test harness. + +---- + +## 💡 `process.mainArgs` Proposal + +> Note: This can be moved forward independently of the `util.parseArgs()` proposal/work. + +### Implementation: + +```javascript +process.mainArgs = process.argv.slice(process._exec ? 1 : 2) +``` + +---- + +## 📃 Examples + +```js +const { parseArgs } = require('@pkgjs/parseargs'); +``` + +```js +const { parseArgs } = require('@pkgjs/parseargs'); +// specify the options that may be used +const options = { + foo: { type: 'string'}, + bar: { type: 'boolean' }, +}; +const args = ['--foo=a', '--bar']; +const { values, positionals } = parseArgs({ args, options }); +// values = { foo: 'a', bar: true } +// positionals = [] +``` + +```js +const { parseArgs } = require('@pkgjs/parseargs'); +// type:string & multiple +const options = { + foo: { + type: 'string', + multiple: true, + }, +}; +const args = ['--foo=a', '--foo', 'b']; +const { values, positionals } = parseArgs({ args, options }); +// values = { foo: [ 'a', 'b' ] } +// positionals = [] +``` + +```js +const { parseArgs } = require('@pkgjs/parseargs'); +// shorts +const options = { + foo: { + short: 'f', + type: 'boolean' + }, +}; +const args = ['-f', 'b']; +const { values, positionals } = parseArgs({ args, options, allowPositionals: true }); +// values = { foo: true } +// positionals = ['b'] +``` + +```js +const { parseArgs } = require('@pkgjs/parseargs'); +// unconfigured +const options = {}; +const args = ['-f', '--foo=a', '--bar', 'b']; +const { values, positionals } = parseArgs({ strict: false, args, options, allowPositionals: true }); +// values = { f: true, foo: 'a', bar: true } +// positionals = ['b'] +``` + +---- + +## F.A.Qs + +- Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`? + - yes +- Does the parser execute a function? + - no +- Does the parser execute one of several functions, depending on input? + - no +- Can subcommands take options that are distinct from the main command? + - no +- Does it output generated help when no options match? + - no +- Does it generated short usage? Like: `usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]` + - no (no usage/help at all) +- Does the user provide the long usage text? For each option? For the whole command? + - no +- Do subcommands (if implemented) have their own usage output? + - no +- Does usage print if the user runs `cmd --help`? + - no +- Does it set `process.exitCode`? + - no +- Does usage print to stderr or stdout? + - N/A +- Does it check types? (Say, specify that an option is a boolean, number, etc.) + - no +- Can an option have more than one type? (string or false, for example) + - no +- Can the user define a type? (Say, `type: path` to call `path.resolve()` on the argument.) + - no +- Does a `--foo=0o22` mean 0, 22, 18, or "0o22"? + - `"0o22"` +- Does it coerce types? + - no +- Does `--no-foo` coerce to `--foo=false`? For all options? Only boolean options? + - no, it sets `{values:{'no-foo': true}}` +- Is `--foo` the same as `--foo=true`? Only for known booleans? Only at the end? + - no, they are not the same. There is no special handling of `true` as a value so it is just another string. +- Does it read environment variables? Ie, is `FOO=1 cmd` the same as `cmd --foo=1`? + - no +- Do unknown arguments raise an error? Are they parsed? Are they treated as positional arguments? + - no, they are parsed, not treated as positionals +- Does `--` signal the end of options? + - yes +- Is `--` included as a positional? + - no +- Is `program -- foo` the same as `program foo`? + - yes, both store `{positionals:['foo']}` +- Does the API specify whether a `--` was present/relevant? + - no +- Is `-bar` the same as `--bar`? + - no, `-bar` is a short option or options, with expansion logic that follows the + [Utility Syntax Guidelines in POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). `-bar` expands to `-b`, `-a`, `-r`. +- Is `---foo` the same as `--foo`? + - no + - the first is a long option named `'-foo'` + - the second is a long option named `'foo'` +- Is `-` a positional? ie, `bash some-test.sh | tap -` + - yes + +## Links & Resources + +* [Initial Tooling Issue](https://github.com/nodejs/tooling/issues/19) +* [Initial Proposal](https://github.com/nodejs/node/pull/35015) +* [parseArgs Proposal](https://github.com/nodejs/node/pull/42675) + +[coverage-image]: https://img.shields.io/nycrc/pkgjs/parseargs +[coverage-url]: https://github.com/pkgjs/parseargs/blob/main/.nycrc +[pkgjs/parseargs]: https://github.com/pkgjs/parseargs diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/is-default-value.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/is-default-value.js new file mode 100644 index 00000000..0a67972b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/is-default-value.js @@ -0,0 +1,25 @@ +'use strict'; + +// This example shows how to understand if a default value is used or not. + +// 1. const { parseArgs } = require('node:util'); // from node +// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package +const { parseArgs } = require('..'); // in repo + +const options = { + file: { short: 'f', type: 'string', default: 'FOO' }, +}; + +const { values, tokens } = parseArgs({ options, tokens: true }); + +const isFileDefault = !tokens.some((token) => token.kind === 'option' && + token.name === 'file' +); + +console.log(values); +console.log(`Is the file option [${values.file}] the default value? ${isFileDefault}`); + +// Try the following: +// node is-default-value.js +// node is-default-value.js -f FILE +// node is-default-value.js --file FILE diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/limit-long-syntax.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/limit-long-syntax.js new file mode 100644 index 00000000..943e643e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/limit-long-syntax.js @@ -0,0 +1,35 @@ +'use strict'; + +// This is an example of using tokens to add a custom behaviour. +// +// Require the use of `=` for long options and values by blocking +// the use of space separated values. +// So allow `--foo=bar`, and not allow `--foo bar`. +// +// Note: this is not a common behaviour, most CLIs allow both forms. + +// 1. const { parseArgs } = require('node:util'); // from node +// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package +const { parseArgs } = require('..'); // in repo + +const options = { + file: { short: 'f', type: 'string' }, + log: { type: 'string' }, +}; + +const { values, tokens } = parseArgs({ options, tokens: true }); + +const badToken = tokens.find((token) => token.kind === 'option' && + token.value != null && + token.rawName.startsWith('--') && + !token.inlineValue +); +if (badToken) { + throw new Error(`Option value for '${badToken.rawName}' must be inline, like '${badToken.rawName}=VALUE'`); +} + +console.log(values); + +// Try the following: +// node limit-long-syntax.js -f FILE --log=LOG +// node limit-long-syntax.js --file FILE diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/negate.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/negate.js new file mode 100644 index 00000000..b6634690 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/negate.js @@ -0,0 +1,43 @@ +'use strict'; + +// This example is used in the documentation. + +// How might I add my own support for --no-foo? + +// 1. const { parseArgs } = require('node:util'); // from node +// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package +const { parseArgs } = require('..'); // in repo + +const options = { + 'color': { type: 'boolean' }, + 'no-color': { type: 'boolean' }, + 'logfile': { type: 'string' }, + 'no-logfile': { type: 'boolean' }, +}; +const { values, tokens } = parseArgs({ options, tokens: true }); + +// Reprocess the option tokens and overwrite the returned values. +tokens + .filter((token) => token.kind === 'option') + .forEach((token) => { + if (token.name.startsWith('no-')) { + // Store foo:false for --no-foo + const positiveName = token.name.slice(3); + values[positiveName] = false; + delete values[token.name]; + } else { + // Resave value so last one wins if both --foo and --no-foo. + values[token.name] = token.value ?? true; + } + }); + +const color = values.color; +const logfile = values.logfile ?? 'default.log'; + +console.log({ logfile, color }); + +// Try the following: +// node negate.js +// node negate.js --no-logfile --no-color +// negate.js --logfile=test.log --color +// node negate.js --no-logfile --logfile=test.log --color --no-color diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/no-repeated-options.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/no-repeated-options.js new file mode 100644 index 00000000..0c324688 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/no-repeated-options.js @@ -0,0 +1,31 @@ +'use strict'; + +// This is an example of using tokens to add a custom behaviour. +// +// Throw an error if an option is used more than once. + +// 1. const { parseArgs } = require('node:util'); // from node +// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package +const { parseArgs } = require('..'); // in repo + +const options = { + ding: { type: 'boolean', short: 'd' }, + beep: { type: 'boolean', short: 'b' } +}; +const { values, tokens } = parseArgs({ options, tokens: true }); + +const seenBefore = new Set(); +tokens.forEach((token) => { + if (token.kind !== 'option') return; + if (seenBefore.has(token.name)) { + throw new Error(`option '${token.name}' used multiple times`); + } + seenBefore.add(token.name); +}); + +console.log(values); + +// Try the following: +// node no-repeated-options --ding --beep +// node no-repeated-options --beep -b +// node no-repeated-options -ddd diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/ordered-options.mjs b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/ordered-options.mjs new file mode 100644 index 00000000..8ab7367b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/ordered-options.mjs @@ -0,0 +1,41 @@ +// This is an example of using tokens to add a custom behaviour. +// +// This adds a option order check so that --some-unstable-option +// may only be used after --enable-experimental-options +// +// Note: this is not a common behaviour, the order of different options +// does not usually matter. + +import { parseArgs } from '../index.js'; + +function findTokenIndex(tokens, target) { + return tokens.findIndex((token) => token.kind === 'option' && + token.name === target + ); +} + +const experimentalName = 'enable-experimental-options'; +const unstableName = 'some-unstable-option'; + +const options = { + [experimentalName]: { type: 'boolean' }, + [unstableName]: { type: 'boolean' }, +}; + +const { values, tokens } = parseArgs({ options, tokens: true }); + +const experimentalIndex = findTokenIndex(tokens, experimentalName); +const unstableIndex = findTokenIndex(tokens, unstableName); +if (unstableIndex !== -1 && + ((experimentalIndex === -1) || (unstableIndex < experimentalIndex))) { + throw new Error(`'--${experimentalName}' must be specified before '--${unstableName}'`); +} + +console.log(values); + +/* eslint-disable max-len */ +// Try the following: +// node ordered-options.mjs +// node ordered-options.mjs --some-unstable-option +// node ordered-options.mjs --some-unstable-option --enable-experimental-options +// node ordered-options.mjs --enable-experimental-options --some-unstable-option diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/simple-hard-coded.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/simple-hard-coded.js new file mode 100644 index 00000000..eff04c2a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/simple-hard-coded.js @@ -0,0 +1,26 @@ +'use strict'; + +// This example is used in the documentation. + +// 1. const { parseArgs } = require('node:util'); // from node +// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package +const { parseArgs } = require('..'); // in repo + +const args = ['-f', '--bar', 'b']; +const options = { + foo: { + type: 'boolean', + short: 'f' + }, + bar: { + type: 'string' + } +}; +const { + values, + positionals +} = parseArgs({ args, options }); +console.log(values, positionals); + +// Try the following: +// node simple-hard-coded.js diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/index.js new file mode 100644 index 00000000..b1004c7b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/index.js @@ -0,0 +1,396 @@ +'use strict'; + +const { + ArrayPrototypeForEach, + ArrayPrototypeIncludes, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypePushApply, + ArrayPrototypeShift, + ArrayPrototypeSlice, + ArrayPrototypeUnshiftApply, + ObjectEntries, + ObjectPrototypeHasOwnProperty: ObjectHasOwn, + StringPrototypeCharAt, + StringPrototypeIndexOf, + StringPrototypeSlice, + StringPrototypeStartsWith, +} = require('./internal/primordials'); + +const { + validateArray, + validateBoolean, + validateBooleanArray, + validateObject, + validateString, + validateStringArray, + validateUnion, +} = require('./internal/validators'); + +const { + kEmptyObject, +} = require('./internal/util'); + +const { + findLongOptionForShort, + isLoneLongOption, + isLoneShortOption, + isLongOptionAndValue, + isOptionValue, + isOptionLikeValue, + isShortOptionAndValue, + isShortOptionGroup, + useDefaultValueOption, + objectGetOwn, + optionsGetOwn, +} = require('./utils'); + +const { + codes: { + ERR_INVALID_ARG_VALUE, + ERR_PARSE_ARGS_INVALID_OPTION_VALUE, + ERR_PARSE_ARGS_UNKNOWN_OPTION, + ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, + }, +} = require('./internal/errors'); + +function getMainArgs() { + // Work out where to slice process.argv for user supplied arguments. + + // Check node options for scenarios where user CLI args follow executable. + const execArgv = process.execArgv; + if (ArrayPrototypeIncludes(execArgv, '-e') || + ArrayPrototypeIncludes(execArgv, '--eval') || + ArrayPrototypeIncludes(execArgv, '-p') || + ArrayPrototypeIncludes(execArgv, '--print')) { + return ArrayPrototypeSlice(process.argv, 1); + } + + // Normally first two arguments are executable and script, then CLI arguments + return ArrayPrototypeSlice(process.argv, 2); +} + +/** + * In strict mode, throw for possible usage errors like --foo --bar + * + * @param {object} token - from tokens as available from parseArgs + */ +function checkOptionLikeValue(token) { + if (!token.inlineValue && isOptionLikeValue(token.value)) { + // Only show short example if user used short option. + const example = StringPrototypeStartsWith(token.rawName, '--') ? + `'${token.rawName}=-XYZ'` : + `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`; + const errorMessage = `Option '${token.rawName}' argument is ambiguous. +Did you forget to specify the option argument for '${token.rawName}'? +To specify an option argument starting with a dash use ${example}.`; + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage); + } +} + +/** + * In strict mode, throw for usage errors. + * + * @param {object} config - from config passed to parseArgs + * @param {object} token - from tokens as available from parseArgs + */ +function checkOptionUsage(config, token) { + if (!ObjectHasOwn(config.options, token.name)) { + throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( + token.rawName, config.allowPositionals); + } + + const short = optionsGetOwn(config.options, token.name, 'short'); + const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`; + const type = optionsGetOwn(config.options, token.name, 'type'); + if (type === 'string' && typeof token.value !== 'string') { + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} ' argument missing`); + } + // (Idiomatic test for undefined||null, expecting undefined.) + if (type === 'boolean' && token.value != null) { + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`); + } +} + + +/** + * Store the option value in `values`. + * + * @param {string} longOption - long option name e.g. 'foo' + * @param {string|undefined} optionValue - value from user args + * @param {object} options - option configs, from parseArgs({ options }) + * @param {object} values - option values returned in `values` by parseArgs + */ +function storeOption(longOption, optionValue, options, values) { + if (longOption === '__proto__') { + return; // No. Just no. + } + + // We store based on the option value rather than option type, + // preserving the users intent for author to deal with. + const newValue = optionValue ?? true; + if (optionsGetOwn(options, longOption, 'multiple')) { + // Always store value in array, including for boolean. + // values[longOption] starts out not present, + // first value is added as new array [newValue], + // subsequent values are pushed to existing array. + // (note: values has null prototype, so simpler usage) + if (values[longOption]) { + ArrayPrototypePush(values[longOption], newValue); + } else { + values[longOption] = [newValue]; + } + } else { + values[longOption] = newValue; + } +} + +/** + * Store the default option value in `values`. + * + * @param {string} longOption - long option name e.g. 'foo' + * @param {string + * | boolean + * | string[] + * | boolean[]} optionValue - default value from option config + * @param {object} values - option values returned in `values` by parseArgs + */ +function storeDefaultOption(longOption, optionValue, values) { + if (longOption === '__proto__') { + return; // No. Just no. + } + + values[longOption] = optionValue; +} + +/** + * Process args and turn into identified tokens: + * - option (along with value, if any) + * - positional + * - option-terminator + * + * @param {string[]} args - from parseArgs({ args }) or mainArgs + * @param {object} options - option configs, from parseArgs({ options }) + */ +function argsToTokens(args, options) { + const tokens = []; + let index = -1; + let groupCount = 0; + + const remainingArgs = ArrayPrototypeSlice(args); + while (remainingArgs.length > 0) { + const arg = ArrayPrototypeShift(remainingArgs); + const nextArg = remainingArgs[0]; + if (groupCount > 0) + groupCount--; + else + index++; + + // Check if `arg` is an options terminator. + // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html + if (arg === '--') { + // Everything after a bare '--' is considered a positional argument. + ArrayPrototypePush(tokens, { kind: 'option-terminator', index }); + ArrayPrototypePushApply( + tokens, ArrayPrototypeMap(remainingArgs, (arg) => { + return { kind: 'positional', index: ++index, value: arg }; + }) + ); + break; // Finished processing args, leave while loop. + } + + if (isLoneShortOption(arg)) { + // e.g. '-f' + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + let value; + let inlineValue; + if (optionsGetOwn(options, longOption, 'type') === 'string' && + isOptionValue(nextArg)) { + // e.g. '-f', 'bar' + value = ArrayPrototypeShift(remainingArgs); + inlineValue = false; + } + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: arg, + index, value, inlineValue }); + if (value != null) ++index; + continue; + } + + if (isShortOptionGroup(arg, options)) { + // Expand -fXzy to -f -X -z -y + const expanded = []; + for (let index = 1; index < arg.length; index++) { + const shortOption = StringPrototypeCharAt(arg, index); + const longOption = findLongOptionForShort(shortOption, options); + if (optionsGetOwn(options, longOption, 'type') !== 'string' || + index === arg.length - 1) { + // Boolean option, or last short in group. Well formed. + ArrayPrototypePush(expanded, `-${shortOption}`); + } else { + // String option in middle. Yuck. + // Expand -abfFILE to -a -b -fFILE + ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`); + break; // finished short group + } + } + ArrayPrototypeUnshiftApply(remainingArgs, expanded); + groupCount = expanded.length; + continue; + } + + if (isShortOptionAndValue(arg, options)) { + // e.g. -fFILE + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + const value = StringPrototypeSlice(arg, 2); + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: `-${shortOption}`, + index, value, inlineValue: true }); + continue; + } + + if (isLoneLongOption(arg)) { + // e.g. '--foo' + const longOption = StringPrototypeSlice(arg, 2); + let value; + let inlineValue; + if (optionsGetOwn(options, longOption, 'type') === 'string' && + isOptionValue(nextArg)) { + // e.g. '--foo', 'bar' + value = ArrayPrototypeShift(remainingArgs); + inlineValue = false; + } + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: arg, + index, value, inlineValue }); + if (value != null) ++index; + continue; + } + + if (isLongOptionAndValue(arg)) { + // e.g. --foo=bar + const equalIndex = StringPrototypeIndexOf(arg, '='); + const longOption = StringPrototypeSlice(arg, 2, equalIndex); + const value = StringPrototypeSlice(arg, equalIndex + 1); + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: `--${longOption}`, + index, value, inlineValue: true }); + continue; + } + + ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg }); + } + + return tokens; +} + +const parseArgs = (config = kEmptyObject) => { + const args = objectGetOwn(config, 'args') ?? getMainArgs(); + const strict = objectGetOwn(config, 'strict') ?? true; + const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict; + const returnTokens = objectGetOwn(config, 'tokens') ?? false; + const options = objectGetOwn(config, 'options') ?? { __proto__: null }; + // Bundle these up for passing to strict-mode checks. + const parseConfig = { args, strict, options, allowPositionals }; + + // Validate input configuration. + validateArray(args, 'args'); + validateBoolean(strict, 'strict'); + validateBoolean(allowPositionals, 'allowPositionals'); + validateBoolean(returnTokens, 'tokens'); + validateObject(options, 'options'); + ArrayPrototypeForEach( + ObjectEntries(options), + ({ 0: longOption, 1: optionConfig }) => { + validateObject(optionConfig, `options.${longOption}`); + + // type is required + const optionType = objectGetOwn(optionConfig, 'type'); + validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']); + + if (ObjectHasOwn(optionConfig, 'short')) { + const shortOption = optionConfig.short; + validateString(shortOption, `options.${longOption}.short`); + if (shortOption.length !== 1) { + throw new ERR_INVALID_ARG_VALUE( + `options.${longOption}.short`, + shortOption, + 'must be a single character' + ); + } + } + + const multipleOption = objectGetOwn(optionConfig, 'multiple'); + if (ObjectHasOwn(optionConfig, 'multiple')) { + validateBoolean(multipleOption, `options.${longOption}.multiple`); + } + + const defaultValue = objectGetOwn(optionConfig, 'default'); + if (defaultValue !== undefined) { + let validator; + switch (optionType) { + case 'string': + validator = multipleOption ? validateStringArray : validateString; + break; + + case 'boolean': + validator = multipleOption ? validateBooleanArray : validateBoolean; + break; + } + validator(defaultValue, `options.${longOption}.default`); + } + } + ); + + // Phase 1: identify tokens + const tokens = argsToTokens(args, options); + + // Phase 2: process tokens into parsed option values and positionals + const result = { + values: { __proto__: null }, + positionals: [], + }; + if (returnTokens) { + result.tokens = tokens; + } + ArrayPrototypeForEach(tokens, (token) => { + if (token.kind === 'option') { + if (strict) { + checkOptionUsage(parseConfig, token); + checkOptionLikeValue(token); + } + storeOption(token.name, token.value, options, result.values); + } else if (token.kind === 'positional') { + if (!allowPositionals) { + throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value); + } + ArrayPrototypePush(result.positionals, token.value); + } + }); + + // Phase 3: fill in default values for missing args + ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, + 1: optionConfig }) => { + const mustSetDefault = useDefaultValueOption(longOption, + optionConfig, + result.values); + if (mustSetDefault) { + storeDefaultOption(longOption, + objectGetOwn(optionConfig, 'default'), + result.values); + } + }); + + + return result; +}; + +module.exports = { + parseArgs, +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/errors.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/errors.js new file mode 100644 index 00000000..e1b237b5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/errors.js @@ -0,0 +1,47 @@ +'use strict'; + +class ERR_INVALID_ARG_TYPE extends TypeError { + constructor(name, expected, actual) { + super(`${name} must be ${expected} got ${actual}`); + this.code = 'ERR_INVALID_ARG_TYPE'; + } +} + +class ERR_INVALID_ARG_VALUE extends TypeError { + constructor(arg1, arg2, expected) { + super(`The property ${arg1} ${expected}. Received '${arg2}'`); + this.code = 'ERR_INVALID_ARG_VALUE'; + } +} + +class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends Error { + constructor(message) { + super(message); + this.code = 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'; + } +} + +class ERR_PARSE_ARGS_UNKNOWN_OPTION extends Error { + constructor(option, allowPositionals) { + const suggestDashDash = allowPositionals ? `. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- ${JSON.stringify(option)}` : ''; + super(`Unknown option '${option}'${suggestDashDash}`); + this.code = 'ERR_PARSE_ARGS_UNKNOWN_OPTION'; + } +} + +class ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL extends Error { + constructor(positional) { + super(`Unexpected argument '${positional}'. This command does not take positional arguments`); + this.code = 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'; + } +} + +module.exports = { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_PARSE_ARGS_INVALID_OPTION_VALUE, + ERR_PARSE_ARGS_UNKNOWN_OPTION, + ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, + } +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/primordials.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/primordials.js new file mode 100644 index 00000000..63e23ab1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/primordials.js @@ -0,0 +1,393 @@ +/* +This file is copied from https://github.com/nodejs/node/blob/v14.19.3/lib/internal/per_context/primordials.js +under the following license: + +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +*/ + +'use strict'; + +/* eslint-disable node-core/prefer-primordials */ + +// This file subclasses and stores the JS builtins that come from the VM +// so that Node.js's builtin modules do not need to later look these up from +// the global proxy, which can be mutated by users. + +// Use of primordials have sometimes a dramatic impact on performance, please +// benchmark all changes made in performance-sensitive areas of the codebase. +// See: https://github.com/nodejs/node/pull/38248 + +const primordials = {}; + +const { + defineProperty: ReflectDefineProperty, + getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor, + ownKeys: ReflectOwnKeys, +} = Reflect; + +// `uncurryThis` is equivalent to `func => Function.prototype.call.bind(func)`. +// It is using `bind.bind(call)` to avoid using `Function.prototype.bind` +// and `Function.prototype.call` after it may have been mutated by users. +const { apply, bind, call } = Function.prototype; +const uncurryThis = bind.bind(call); +primordials.uncurryThis = uncurryThis; + +// `applyBind` is equivalent to `func => Function.prototype.apply.bind(func)`. +// It is using `bind.bind(apply)` to avoid using `Function.prototype.bind` +// and `Function.prototype.apply` after it may have been mutated by users. +const applyBind = bind.bind(apply); +primordials.applyBind = applyBind; + +// Methods that accept a variable number of arguments, and thus it's useful to +// also create `${prefix}${key}Apply`, which uses `Function.prototype.apply`, +// instead of `Function.prototype.call`, and thus doesn't require iterator +// destructuring. +const varargsMethods = [ + // 'ArrayPrototypeConcat' is omitted, because it performs the spread + // on its own for arrays and array-likes with a truthy + // @@isConcatSpreadable symbol property. + 'ArrayOf', + 'ArrayPrototypePush', + 'ArrayPrototypeUnshift', + // 'FunctionPrototypeCall' is omitted, since there's 'ReflectApply' + // and 'FunctionPrototypeApply'. + 'MathHypot', + 'MathMax', + 'MathMin', + 'StringPrototypeConcat', + 'TypedArrayOf', +]; + +function getNewKey(key) { + return typeof key === 'symbol' ? + `Symbol${key.description[7].toUpperCase()}${key.description.slice(8)}` : + `${key[0].toUpperCase()}${key.slice(1)}`; +} + +function copyAccessor(dest, prefix, key, { enumerable, get, set }) { + ReflectDefineProperty(dest, `${prefix}Get${key}`, { + value: uncurryThis(get), + enumerable + }); + if (set !== undefined) { + ReflectDefineProperty(dest, `${prefix}Set${key}`, { + value: uncurryThis(set), + enumerable + }); + } +} + +function copyPropsRenamed(src, dest, prefix) { + for (const key of ReflectOwnKeys(src)) { + const newKey = getNewKey(key); + const desc = ReflectGetOwnPropertyDescriptor(src, key); + if ('get' in desc) { + copyAccessor(dest, prefix, newKey, desc); + } else { + const name = `${prefix}${newKey}`; + ReflectDefineProperty(dest, name, desc); + if (varargsMethods.includes(name)) { + ReflectDefineProperty(dest, `${name}Apply`, { + // `src` is bound as the `this` so that the static `this` points + // to the object it was defined on, + // e.g.: `ArrayOfApply` gets a `this` of `Array`: + value: applyBind(desc.value, src), + }); + } + } + } +} + +function copyPropsRenamedBound(src, dest, prefix) { + for (const key of ReflectOwnKeys(src)) { + const newKey = getNewKey(key); + const desc = ReflectGetOwnPropertyDescriptor(src, key); + if ('get' in desc) { + copyAccessor(dest, prefix, newKey, desc); + } else { + const { value } = desc; + if (typeof value === 'function') { + desc.value = value.bind(src); + } + + const name = `${prefix}${newKey}`; + ReflectDefineProperty(dest, name, desc); + if (varargsMethods.includes(name)) { + ReflectDefineProperty(dest, `${name}Apply`, { + value: applyBind(value, src), + }); + } + } + } +} + +function copyPrototype(src, dest, prefix) { + for (const key of ReflectOwnKeys(src)) { + const newKey = getNewKey(key); + const desc = ReflectGetOwnPropertyDescriptor(src, key); + if ('get' in desc) { + copyAccessor(dest, prefix, newKey, desc); + } else { + const { value } = desc; + if (typeof value === 'function') { + desc.value = uncurryThis(value); + } + + const name = `${prefix}${newKey}`; + ReflectDefineProperty(dest, name, desc); + if (varargsMethods.includes(name)) { + ReflectDefineProperty(dest, `${name}Apply`, { + value: applyBind(value), + }); + } + } + } +} + +// Create copies of configurable value properties of the global object +[ + 'Proxy', + 'globalThis', +].forEach((name) => { + // eslint-disable-next-line no-restricted-globals + primordials[name] = globalThis[name]; +}); + +// Create copies of URI handling functions +[ + decodeURI, + decodeURIComponent, + encodeURI, + encodeURIComponent, +].forEach((fn) => { + primordials[fn.name] = fn; +}); + +// Create copies of the namespace objects +[ + 'JSON', + 'Math', + 'Proxy', + 'Reflect', +].forEach((name) => { + // eslint-disable-next-line no-restricted-globals + copyPropsRenamed(global[name], primordials, name); +}); + +// Create copies of intrinsic objects +[ + 'Array', + 'ArrayBuffer', + 'BigInt', + 'BigInt64Array', + 'BigUint64Array', + 'Boolean', + 'DataView', + 'Date', + 'Error', + 'EvalError', + 'Float32Array', + 'Float64Array', + 'Function', + 'Int16Array', + 'Int32Array', + 'Int8Array', + 'Map', + 'Number', + 'Object', + 'RangeError', + 'ReferenceError', + 'RegExp', + 'Set', + 'String', + 'Symbol', + 'SyntaxError', + 'TypeError', + 'URIError', + 'Uint16Array', + 'Uint32Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'WeakMap', + 'WeakSet', +].forEach((name) => { + // eslint-disable-next-line no-restricted-globals + const original = global[name]; + primordials[name] = original; + copyPropsRenamed(original, primordials, name); + copyPrototype(original.prototype, primordials, `${name}Prototype`); +}); + +// Create copies of intrinsic objects that require a valid `this` to call +// static methods. +// Refs: https://www.ecma-international.org/ecma-262/#sec-promise.all +[ + 'Promise', +].forEach((name) => { + // eslint-disable-next-line no-restricted-globals + const original = global[name]; + primordials[name] = original; + copyPropsRenamedBound(original, primordials, name); + copyPrototype(original.prototype, primordials, `${name}Prototype`); +}); + +// Create copies of abstract intrinsic objects that are not directly exposed +// on the global object. +// Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object +[ + { name: 'TypedArray', original: Reflect.getPrototypeOf(Uint8Array) }, + { name: 'ArrayIterator', original: { + prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()), + } }, + { name: 'StringIterator', original: { + prototype: Reflect.getPrototypeOf(String.prototype[Symbol.iterator]()), + } }, +].forEach(({ name, original }) => { + primordials[name] = original; + // The static %TypedArray% methods require a valid `this`, but can't be bound, + // as they need a subclass constructor as the receiver: + copyPrototype(original, primordials, name); + copyPrototype(original.prototype, primordials, `${name}Prototype`); +}); + +/* eslint-enable node-core/prefer-primordials */ + +const { + ArrayPrototypeForEach, + FunctionPrototypeCall, + Map, + ObjectFreeze, + ObjectSetPrototypeOf, + Set, + SymbolIterator, + WeakMap, + WeakSet, +} = primordials; + +// Because these functions are used by `makeSafe`, which is exposed +// on the `primordials` object, it's important to use const references +// to the primordials that they use: +const createSafeIterator = (factory, next) => { + class SafeIterator { + constructor(iterable) { + this._iterator = factory(iterable); + } + next() { + return next(this._iterator); + } + [SymbolIterator]() { + return this; + } + } + ObjectSetPrototypeOf(SafeIterator.prototype, null); + ObjectFreeze(SafeIterator.prototype); + ObjectFreeze(SafeIterator); + return SafeIterator; +}; + +primordials.SafeArrayIterator = createSafeIterator( + primordials.ArrayPrototypeSymbolIterator, + primordials.ArrayIteratorPrototypeNext +); +primordials.SafeStringIterator = createSafeIterator( + primordials.StringPrototypeSymbolIterator, + primordials.StringIteratorPrototypeNext +); + +const copyProps = (src, dest) => { + ArrayPrototypeForEach(ReflectOwnKeys(src), (key) => { + if (!ReflectGetOwnPropertyDescriptor(dest, key)) { + ReflectDefineProperty( + dest, + key, + ReflectGetOwnPropertyDescriptor(src, key)); + } + }); +}; + +const makeSafe = (unsafe, safe) => { + if (SymbolIterator in unsafe.prototype) { + const dummy = new unsafe(); + let next; // We can reuse the same `next` method. + + ArrayPrototypeForEach(ReflectOwnKeys(unsafe.prototype), (key) => { + if (!ReflectGetOwnPropertyDescriptor(safe.prototype, key)) { + const desc = ReflectGetOwnPropertyDescriptor(unsafe.prototype, key); + if ( + typeof desc.value === 'function' && + desc.value.length === 0 && + SymbolIterator in (FunctionPrototypeCall(desc.value, dummy) ?? {}) + ) { + const createIterator = uncurryThis(desc.value); + next = next ?? uncurryThis(createIterator(dummy).next); + const SafeIterator = createSafeIterator(createIterator, next); + desc.value = function() { + return new SafeIterator(this); + }; + } + ReflectDefineProperty(safe.prototype, key, desc); + } + }); + } else { + copyProps(unsafe.prototype, safe.prototype); + } + copyProps(unsafe, safe); + + ObjectSetPrototypeOf(safe.prototype, null); + ObjectFreeze(safe.prototype); + ObjectFreeze(safe); + return safe; +}; +primordials.makeSafe = makeSafe; + +// Subclass the constructors because we need to use their prototype +// methods later. +// Defining the `constructor` is necessary here to avoid the default +// constructor which uses the user-mutable `%ArrayIteratorPrototype%.next`. +primordials.SafeMap = makeSafe( + Map, + class SafeMap extends Map { + constructor(i) { super(i); } // eslint-disable-line no-useless-constructor + } +); +primordials.SafeWeakMap = makeSafe( + WeakMap, + class SafeWeakMap extends WeakMap { + constructor(i) { super(i); } // eslint-disable-line no-useless-constructor + } +); +primordials.SafeSet = makeSafe( + Set, + class SafeSet extends Set { + constructor(i) { super(i); } // eslint-disable-line no-useless-constructor + } +); +primordials.SafeWeakSet = makeSafe( + WeakSet, + class SafeWeakSet extends WeakSet { + constructor(i) { super(i); } // eslint-disable-line no-useless-constructor + } +); + +ObjectSetPrototypeOf(primordials, null); +ObjectFreeze(primordials); + +module.exports = primordials; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/util.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/util.js new file mode 100644 index 00000000..b9b8fe5b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/util.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a placeholder for util.js in node.js land. + +const { + ObjectCreate, + ObjectFreeze, +} = require('./primordials'); + +const kEmptyObject = ObjectFreeze(ObjectCreate(null)); + +module.exports = { + kEmptyObject, +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/validators.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/validators.js new file mode 100644 index 00000000..b5ac4fb5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/validators.js @@ -0,0 +1,89 @@ +'use strict'; + +// This file is a proxy of the original file located at: +// https://github.com/nodejs/node/blob/main/lib/internal/validators.js +// Every addition or modification to this file must be evaluated +// during the PR review. + +const { + ArrayIsArray, + ArrayPrototypeIncludes, + ArrayPrototypeJoin, +} = require('./primordials'); + +const { + codes: { + ERR_INVALID_ARG_TYPE + } +} = require('./errors'); + +function validateString(value, name) { + if (typeof value !== 'string') { + throw new ERR_INVALID_ARG_TYPE(name, 'String', value); + } +} + +function validateUnion(value, name, union) { + if (!ArrayPrototypeIncludes(union, value)) { + throw new ERR_INVALID_ARG_TYPE(name, `('${ArrayPrototypeJoin(union, '|')}')`, value); + } +} + +function validateBoolean(value, name) { + if (typeof value !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE(name, 'Boolean', value); + } +} + +function validateArray(value, name) { + if (!ArrayIsArray(value)) { + throw new ERR_INVALID_ARG_TYPE(name, 'Array', value); + } +} + +function validateStringArray(value, name) { + validateArray(value, name); + for (let i = 0; i < value.length; i++) { + validateString(value[i], `${name}[${i}]`); + } +} + +function validateBooleanArray(value, name) { + validateArray(value, name); + for (let i = 0; i < value.length; i++) { + validateBoolean(value[i], `${name}[${i}]`); + } +} + +/** + * @param {unknown} value + * @param {string} name + * @param {{ + * allowArray?: boolean, + * allowFunction?: boolean, + * nullable?: boolean + * }} [options] + */ +function validateObject(value, name, options) { + const useDefaultOptions = options == null; + const allowArray = useDefaultOptions ? false : options.allowArray; + const allowFunction = useDefaultOptions ? false : options.allowFunction; + const nullable = useDefaultOptions ? false : options.nullable; + if ((!nullable && value === null) || + (!allowArray && ArrayIsArray(value)) || + (typeof value !== 'object' && ( + !allowFunction || typeof value !== 'function' + ))) { + throw new ERR_INVALID_ARG_TYPE(name, 'Object', value); + } +} + +module.exports = { + validateArray, + validateObject, + validateString, + validateStringArray, + validateUnion, + validateBoolean, + validateBooleanArray, +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/package.json new file mode 100644 index 00000000..0bcc05c0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/package.json @@ -0,0 +1,36 @@ +{ + "name": "@pkgjs/parseargs", + "version": "0.11.0", + "description": "Polyfill of future proposal for `util.parseArgs()`", + "engines": { + "node": ">=14" + }, + "main": "index.js", + "exports": { + ".": "./index.js", + "./package.json": "./package.json" + }, + "scripts": { + "coverage": "c8 --check-coverage tape 'test/*.js'", + "test": "c8 tape 'test/*.js'", + "posttest": "eslint .", + "fix": "npm run posttest -- --fix" + }, + "repository": { + "type": "git", + "url": "git@github.com:pkgjs/parseargs.git" + }, + "keywords": [], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/pkgjs/parseargs/issues" + }, + "homepage": "https://github.com/pkgjs/parseargs#readme", + "devDependencies": { + "c8": "^7.10.0", + "eslint": "^8.2.0", + "eslint-plugin-node-core": "iansu/eslint-plugin-node-core", + "tape": "^5.2.2" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/utils.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/utils.js new file mode 100644 index 00000000..d7f420a2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/utils.js @@ -0,0 +1,198 @@ +'use strict'; + +const { + ArrayPrototypeFind, + ObjectEntries, + ObjectPrototypeHasOwnProperty: ObjectHasOwn, + StringPrototypeCharAt, + StringPrototypeIncludes, + StringPrototypeStartsWith, +} = require('./internal/primordials'); + +const { + validateObject, +} = require('./internal/validators'); + +// These are internal utilities to make the parsing logic easier to read, and +// add lots of detail for the curious. They are in a separate file to allow +// unit testing, although that is not essential (this could be rolled into +// main file and just tested implicitly via API). +// +// These routines are for internal use, not for export to client. + +/** + * Return the named property, but only if it is an own property. + */ +function objectGetOwn(obj, prop) { + if (ObjectHasOwn(obj, prop)) + return obj[prop]; +} + +/** + * Return the named options property, but only if it is an own property. + */ +function optionsGetOwn(options, longOption, prop) { + if (ObjectHasOwn(options, longOption)) + return objectGetOwn(options[longOption], prop); +} + +/** + * Determines if the argument may be used as an option value. + * @example + * isOptionValue('V') // returns true + * isOptionValue('-v') // returns true (greedy) + * isOptionValue('--foo') // returns true (greedy) + * isOptionValue(undefined) // returns false + */ +function isOptionValue(value) { + if (value == null) return false; + + // Open Group Utility Conventions are that an option-argument + // is the argument after the option, and may start with a dash. + return true; // greedy! +} + +/** + * Detect whether there is possible confusion and user may have omitted + * the option argument, like `--port --verbose` when `port` of type:string. + * In strict mode we throw errors if value is option-like. + */ +function isOptionLikeValue(value) { + if (value == null) return false; + + return value.length > 1 && StringPrototypeCharAt(value, 0) === '-'; +} + +/** + * Determines if `arg` is just a short option. + * @example '-f' + */ +function isLoneShortOption(arg) { + return arg.length === 2 && + StringPrototypeCharAt(arg, 0) === '-' && + StringPrototypeCharAt(arg, 1) !== '-'; +} + +/** + * Determines if `arg` is a lone long option. + * @example + * isLoneLongOption('a') // returns false + * isLoneLongOption('-a') // returns false + * isLoneLongOption('--foo') // returns true + * isLoneLongOption('--foo=bar') // returns false + */ +function isLoneLongOption(arg) { + return arg.length > 2 && + StringPrototypeStartsWith(arg, '--') && + !StringPrototypeIncludes(arg, '=', 3); +} + +/** + * Determines if `arg` is a long option and value in the same argument. + * @example + * isLongOptionAndValue('--foo') // returns false + * isLongOptionAndValue('--foo=bar') // returns true + */ +function isLongOptionAndValue(arg) { + return arg.length > 2 && + StringPrototypeStartsWith(arg, '--') && + StringPrototypeIncludes(arg, '=', 3); +} + +/** + * Determines if `arg` is a short option group. + * + * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). + * One or more options without option-arguments, followed by at most one + * option that takes an option-argument, should be accepted when grouped + * behind one '-' delimiter. + * @example + * isShortOptionGroup('-a', {}) // returns false + * isShortOptionGroup('-ab', {}) // returns true + * // -fb is an option and a value, not a short option group + * isShortOptionGroup('-fb', { + * options: { f: { type: 'string' } } + * }) // returns false + * isShortOptionGroup('-bf', { + * options: { f: { type: 'string' } } + * }) // returns true + * // -bfb is an edge case, return true and caller sorts it out + * isShortOptionGroup('-bfb', { + * options: { f: { type: 'string' } } + * }) // returns true + */ +function isShortOptionGroup(arg, options) { + if (arg.length <= 2) return false; + if (StringPrototypeCharAt(arg, 0) !== '-') return false; + if (StringPrototypeCharAt(arg, 1) === '-') return false; + + const firstShort = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(firstShort, options); + return optionsGetOwn(options, longOption, 'type') !== 'string'; +} + +/** + * Determine if arg is a short string option followed by its value. + * @example + * isShortOptionAndValue('-a', {}); // returns false + * isShortOptionAndValue('-ab', {}); // returns false + * isShortOptionAndValue('-fFILE', { + * options: { foo: { short: 'f', type: 'string' }} + * }) // returns true + */ +function isShortOptionAndValue(arg, options) { + validateObject(options, 'options'); + + if (arg.length <= 2) return false; + if (StringPrototypeCharAt(arg, 0) !== '-') return false; + if (StringPrototypeCharAt(arg, 1) === '-') return false; + + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + return optionsGetOwn(options, longOption, 'type') === 'string'; +} + +/** + * Find the long option associated with a short option. Looks for a configured + * `short` and returns the short option itself if a long option is not found. + * @example + * findLongOptionForShort('a', {}) // returns 'a' + * findLongOptionForShort('b', { + * options: { bar: { short: 'b' } } + * }) // returns 'bar' + */ +function findLongOptionForShort(shortOption, options) { + validateObject(options, 'options'); + const longOptionEntry = ArrayPrototypeFind( + ObjectEntries(options), + ({ 1: optionConfig }) => objectGetOwn(optionConfig, 'short') === shortOption + ); + return longOptionEntry?.[0] ?? shortOption; +} + +/** + * Check if the given option includes a default value + * and that option has not been set by the input args. + * + * @param {string} longOption - long option name e.g. 'foo' + * @param {object} optionConfig - the option configuration properties + * @param {object} values - option values returned in `values` by parseArgs + */ +function useDefaultValueOption(longOption, optionConfig, values) { + return objectGetOwn(optionConfig, 'default') !== undefined && + values[longOption] === undefined; +} + +module.exports = { + findLongOptionForShort, + isLoneLongOption, + isLoneShortOption, + isLongOptionAndValue, + isOptionValue, + isOptionLikeValue, + isShortOptionAndValue, + isShortOptionGroup, + useDefaultValueOption, + objectGetOwn, + optionsGetOwn, +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/README.md new file mode 100644 index 00000000..cabe280f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `@rollup/rollup-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `rollup` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/package.json new file mode 100644 index 00000000..1ca44457 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/package.json @@ -0,0 +1,25 @@ +{ + "name": "@rollup/rollup-linux-x64-gnu", + "version": "4.59.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "rollup.linux-x64-gnu.node" + ], + "description": "Native bindings for Rollup", + "author": "Lukas Taegert-Atkinson", + "homepage": "https://rollupjs.org/", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/rollup/rollup.git" + }, + "libc": [ + "glibc" + ], + "main": "./rollup.linux-x64-gnu.node" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node new file mode 100644 index 00000000..59216bda Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/README.md new file mode 100644 index 00000000..5848a6c6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/README.md @@ -0,0 +1,3 @@ +# `@rollup/rollup-linux-x64-musl` + +This is the **x86_64-unknown-linux-musl** binary for `rollup` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/package.json new file mode 100644 index 00000000..31ff5f55 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/package.json @@ -0,0 +1,25 @@ +{ + "name": "@rollup/rollup-linux-x64-musl", + "version": "4.59.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "rollup.linux-x64-musl.node" + ], + "description": "Native bindings for Rollup", + "author": "Lukas Taegert-Atkinson", + "homepage": "https://rollupjs.org/", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/rollup/rollup.git" + }, + "libc": [ + "musl" + ], + "main": "./rollup.linux-x64-musl.node" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node new file mode 100644 index 00000000..0f891abd Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node differ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/LICENSE new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/README.md new file mode 100644 index 00000000..0c319f8a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/README.md @@ -0,0 +1,15 @@ +# Installation +> `npm install --save @types/diff` + +# Summary +This package contains type definitions for diff (https://github.com/kpdecker/jsdiff). + +# Details +Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/diff. + +### Additional Details + * Last updated: Mon, 07 Oct 2024 22:07:58 GMT + * Dependencies: none + +# Credits +These definitions were written by [vvakame](https://github.com/vvakame), [szdc](https://github.com/szdc), [BendingBender](https://github.com/BendingBender), and [Piotr Błażejewicz](https://github.com/peterblazejewicz). diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.mts new file mode 100644 index 00000000..8e893de1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.mts @@ -0,0 +1 @@ +export * from "./index.js"; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.ts new file mode 100644 index 00000000..b55974ee --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.ts @@ -0,0 +1,415 @@ +export as namespace Diff; + +export type Callback = (err: undefined, value?: Change[]) => void; + +export interface CallbackOptions { + /** + * Callback to call with the result instead of returning the result directly. + */ + callback: Callback; +} + +export interface BaseOptions { + /** + * `true` to ignore casing difference. + * @default false + */ + ignoreCase?: boolean | undefined; + + /** + * a number specifying the maximum edit distance to consider between the old and new texts. If the edit distance is higher than this, jsdiff will return `undefined` instead of a diff. You can use this to limit the computational cost of diffing large, very different texts by giving up early if the cost will be huge. Works for functions that return change objects and also for `structuredPatch`, but not other patch-generation functions. + */ + maxEditLength?: number | undefined; +} + +export interface WordsOptions extends BaseOptions { + /** + * `true` to ignore leading and trailing whitespace. This is the same as `diffWords()`. + */ + ignoreWhitespace?: boolean | undefined; +} + +export interface LinesOptions extends BaseOptions { + /** + * `true` to ignore leading and trailing whitespace. This is the same as `diffTrimmedLines()`. + */ + ignoreWhitespace?: boolean | undefined; + + /** + * `true` to treat newline characters as separate tokens. This allows for changes to the newline structure + * to occur independently of the line content and to be treated as such. In general this is the more + * human friendly form of `diffLines()` and `diffLines()` is better suited for patches and other computer + * friendly output. + */ + newlineIsToken?: boolean | undefined; +} + +export interface JsonOptions extends LinesOptions { + /** + * Replacer used to stringify the properties of the passed objects. + */ + stringifyReplacer?: ((key: string, value: any) => any) | undefined; + + /** + * The value to use when `undefined` values in the passed objects are encountered during stringification. + * Will only be used if `stringifyReplacer` option wasn't specified. + * @default undefined + */ + undefinedReplacement?: any; +} + +export interface ArrayOptions extends BaseOptions { + /** + * Comparator for custom equality checks. + */ + comparator?: ((left: TLeft, right: TRight) => boolean) | undefined; +} + +export interface PatchOptions extends LinesOptions { + /** + * Describes how many lines of context should be included. + * @default 4 + */ + context?: number | undefined; +} + +export interface ApplyPatchOptions { + /** + * Number of lines that are allowed to differ before rejecting a patch. + * @default 0 + */ + fuzzFactor?: number | undefined; + + /** + * Callback used to compare to given lines to determine if they should be considered equal when patching. + * Should return `false` if the lines should be rejected. + * + * @default strict equality + */ + compareLine?: + | (( + lineNumber: number, + line: string, + operation: "-" | " ", + patchContent: string, + ) => boolean) + | undefined; +} + +export interface ApplyPatchesOptions extends ApplyPatchOptions { + loadFile(index: ParsedDiff, callback: (err: any, data: string) => void): void; + patched(index: ParsedDiff, content: string, callback: (err: any) => void): void; + complete(err: any): void; +} + +export interface Change { + count?: number | undefined; + /** + * Text content. + */ + value: string; + /** + * `true` if the value was inserted into the new string. + */ + added?: boolean | undefined; + /** + * `true` if the value was removed from the old string. + */ + removed?: boolean | undefined; +} + +export interface ArrayChange { + value: T[]; + count?: number | undefined; + added?: boolean | undefined; + removed?: boolean | undefined; +} + +export interface ParsedDiff { + index?: string | undefined; + oldFileName?: string | undefined; + newFileName?: string | undefined; + oldHeader?: string | undefined; + newHeader?: string | undefined; + hunks: Hunk[]; +} + +export interface Hunk { + oldStart: number; + oldLines: number; + newStart: number; + newLines: number; + lines: string[]; + // Line Delimiters is only returned by parsePatch() + linedelimiters?: string[]; +} + +export interface BestPath { + newPos: number; + components: Change[]; +} + +export class Diff { + diff( + oldString: string, + newString: string, + options?: Callback | (ArrayOptions & Partial), + ): Change[]; + + pushComponent(components: Change[], added: boolean, removed: boolean): void; + + extractCommon( + basePath: BestPath, + newString: string, + oldString: string, + diagonalPath: number, + ): number; + + equals(left: any, right: any): boolean; + + removeEmpty(array: any[]): any[]; + + castInput(value: any): any; + + join(chars: string[]): string; + + tokenize(value: string): any; // return types are string or string[] +} + +/** + * Diffs two blocks of text, comparing character by character. + * + * @returns A list of change objects. + */ +export function diffChars(oldStr: string, newStr: string, options?: BaseOptions): Change[]; +export function diffChars( + oldStr: string, + newStr: string, + options: Callback | (BaseOptions & CallbackOptions), +): void; + +/** + * Diffs two blocks of text, comparing word by word, ignoring whitespace. + * + * @returns A list of change objects. + */ +export function diffWords(oldStr: string, newStr: string, options?: WordsOptions): Change[]; +export function diffWords( + oldStr: string, + newStr: string, + options: Callback | (WordsOptions & CallbackOptions), +): void; + +/** + * Diffs two blocks of text, comparing word by word, treating whitespace as significant. + * + * @returns A list of change objects. + */ +export function diffWordsWithSpace( + oldStr: string, + newStr: string, + options?: WordsOptions, +): Change[]; +export function diffWordsWithSpace( + oldStr: string, + newStr: string, + options: Callback | (WordsOptions & CallbackOptions), +): void; + +/** + * Diffs two blocks of text, comparing line by line. + * + * @returns A list of change objects. + */ +export function diffLines(oldStr: string, newStr: string, options?: LinesOptions): Change[]; +export function diffLines( + oldStr: string, + newStr: string, + options: Callback | (LinesOptions & CallbackOptions), +): void; + +/** + * Diffs two blocks of text, comparing line by line, ignoring leading and trailing whitespace. + * + * @returns A list of change objects. + */ +export function diffTrimmedLines(oldStr: string, newStr: string, options?: LinesOptions): Change[]; +export function diffTrimmedLines( + oldStr: string, + newStr: string, + options: Callback | (LinesOptions & CallbackOptions), +): void; + +/** + * Diffs two blocks of text, comparing sentence by sentence. + * + * @returns A list of change objects. + */ +export function diffSentences(oldStr: string, newStr: string, options?: BaseOptions): Change[]; +export function diffSentences( + oldStr: string, + newStr: string, + options: Callback | (BaseOptions & CallbackOptions), +): void; + +/** + * Diffs two blocks of text, comparing CSS tokens. + * + * @returns A list of change objects. + */ +export function diffCss(oldStr: string, newStr: string, options?: BaseOptions): Change[]; +export function diffCss( + oldStr: string, + newStr: string, + options: Callback | (BaseOptions & CallbackOptions), +): void; + +/** + * Diffs two JSON objects, comparing the fields defined on each. The order of fields, etc does not matter + * in this comparison. + * + * @returns A list of change objects. + */ +export function diffJson( + oldObj: string | object, + newObj: string | object, + options?: JsonOptions, +): Change[]; +export function diffJson( + oldObj: string | object, + newObj: string | object, + options: Callback | (JsonOptions & CallbackOptions), +): void; + +/** + * Diffs two arrays, comparing each item for strict equality (`===`). + * + * @returns A list of change objects. + */ +export function diffArrays( + oldArr: TOld[], + newArr: TNew[], + options?: ArrayOptions, +): Array>; + +/** + * Creates a unified diff patch. + * + * @param oldFileName String to be output in the filename section of the patch for the removals. + * @param newFileName String to be output in the filename section of the patch for the additions. + * @param oldStr Original string value. + * @param newStr New string value. + * @param oldHeader Additional information to include in the old file header. + * @param newHeader Additional information to include in the new file header. + */ +export function createTwoFilesPatch( + oldFileName: string, + newFileName: string, + oldStr: string, + newStr: string, + oldHeader?: string, + newHeader?: string, + options?: PatchOptions, +): string; + +/** + * Creates a unified diff patch. + * Just like `createTwoFilesPatch()`, but with `oldFileName` being equal to `newFileName`. + * + * @param fileName String to be output in the filename section. + * @param oldStr Original string value. + * @param newStr New string value. + * @param oldHeader Additional information to include in the old file header. + * @param newHeader Additional information to include in the new file header. + */ +export function createPatch( + fileName: string, + oldStr: string, + newStr: string, + oldHeader?: string, + newHeader?: string, + options?: PatchOptions, +): string; + +/** + * This method is similar to `createTwoFilesPatch()`, but returns a data structure suitable for further processing. + * Parameters are the same as `createTwoFilesPatch()`. + * + * @param oldFileName String to be output in the `oldFileName` hunk property. + * @param newFileName String to be output in the `newFileName` hunk property. + * @param oldStr Original string value. + * @param newStr New string value. + * @param oldHeader Additional information to include in the `oldHeader` hunk property. + * @param newHeader Additional information to include in the `newHeader` hunk property. + * @returns An object with an array of hunk objects. + */ +export function structuredPatch( + oldFileName: string, + newFileName: string, + oldStr: string, + newStr: string, + oldHeader?: string, + newHeader?: string, + options?: PatchOptions, +): ParsedDiff; + +/** + * Applies a unified diff patch. + * + * @param patch May be a string diff or the output from the `parsePatch()` or `structuredPatch()` methods. + * @returns A string containing new version of provided data. false when failed + */ +export function applyPatch( + source: string, + patch: string | ParsedDiff | [ParsedDiff], + options?: ApplyPatchOptions, +): string | false; + +/** + * Applies one or more patches. + * This method will iterate over the contents of the patch and apply to data provided through callbacks. + * + * The general flow for each patch index is: + * + * 1. `options.loadFile(index, callback)` is called. The caller should then load the contents of the file + * and then pass that to the `callback(err, data)` callback. Passing an `err` will terminate further patch execution. + * 2. `options.patched(index, content, callback)` is called once the patch has been applied. `content` will be + * the return value from `applyPatch()`. When it's ready, the caller should call `callback(err)` callback. + * Passing an `err` will terminate further patch execution. + * 3. Once all patches have been applied or an error occurs, the `options.complete(err)` callback is made. + */ +export function applyPatches(patch: string | ParsedDiff[], options: ApplyPatchesOptions): void; + +/** + * Parses a patch into structured data. + * + * @returns A JSON object representation of the a patch, suitable for use with the `applyPatch()` method. + */ +export function parsePatch(diffStr: string, options?: { strict?: boolean | undefined }): ParsedDiff[]; + +/** + * Converts a list of changes to a serialized XML format. + */ +export function convertChangesToXML(changes: Change[]): string; + +/** + * Converts a list of changes to [DMP](http://code.google.com/p/google-diff-match-patch/wiki/API) format. + */ +export function convertChangesToDMP(changes: Change[]): Array<[1 | 0 | -1, string]>; + +export function merge(mine: string, theirs: string, base: string): ParsedDiff; + +/** + * Returns a new structured patch which when applied will undo the original `patch`. + * `patch` may be either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`). + */ +export function reversePatch(patch: ParsedDiff | ParsedDiff[]): ParsedDiff; + +export function canonicalize(obj: any, stack: any[], replacementStack: any[]): any; + +/** + * creates a unified diff patch. + * patch may be either a single structured patch object (as returned by structuredPatch) + * or an array of them (as returned by parsePatch). + */ +export function formatPatch(patch: ParsedDiff | ParsedDiff[]): string; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/package.json new file mode 100644 index 00000000..58c6bec5 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/package.json @@ -0,0 +1,47 @@ +{ + "name": "@types/diff", + "version": "5.2.3", + "description": "TypeScript definitions for diff", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/diff", + "license": "MIT", + "contributors": [ + { + "name": "vvakame", + "githubUsername": "vvakame", + "url": "https://github.com/vvakame" + }, + { + "name": "szdc", + "githubUsername": "szdc", + "url": "https://github.com/szdc" + }, + { + "name": "BendingBender", + "githubUsername": "BendingBender", + "url": "https://github.com/BendingBender" + }, + { + "name": "Piotr Błażejewicz", + "githubUsername": "peterblazejewicz", + "url": "https://github.com/peterblazejewicz" + } + ], + "main": "", + "types": "index.d.ts", + "exports": { + ".": { + "import": "./index.d.mts", + "require": "./index.d.ts" + }, + "./package.json": "./package.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/diff" + }, + "scripts": {}, + "dependencies": {}, + "typesPublisherContentHash": "5e21702dab13a816c53e31a2578ad58f5cc6a5aefefa442a2c5d4bec9e8ea626", + "typeScriptVersion": "4.8" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/LICENSE new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/README.md new file mode 100644 index 00000000..2af760b2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/README.md @@ -0,0 +1,15 @@ +# Installation +> `npm install --save @types/estree` + +# Summary +This package contains type definitions for estree (https://github.com/estree/estree). + +# Details +Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree. + +### Additional Details + * Last updated: Fri, 06 Jun 2025 00:04:33 GMT + * Dependencies: none + +# Credits +These definitions were written by [RReverser](https://github.com/RReverser). diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/flow.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/flow.d.ts new file mode 100644 index 00000000..9d001a92 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/flow.d.ts @@ -0,0 +1,167 @@ +declare namespace ESTree { + interface FlowTypeAnnotation extends Node {} + + interface FlowBaseTypeAnnotation extends FlowTypeAnnotation {} + + interface FlowLiteralTypeAnnotation extends FlowTypeAnnotation, Literal {} + + interface FlowDeclaration extends Declaration {} + + interface AnyTypeAnnotation extends FlowBaseTypeAnnotation {} + + interface ArrayTypeAnnotation extends FlowTypeAnnotation { + elementType: FlowTypeAnnotation; + } + + interface BooleanLiteralTypeAnnotation extends FlowLiteralTypeAnnotation {} + + interface BooleanTypeAnnotation extends FlowBaseTypeAnnotation {} + + interface ClassImplements extends Node { + id: Identifier; + typeParameters?: TypeParameterInstantiation | null; + } + + interface ClassProperty { + key: Expression; + value?: Expression | null; + typeAnnotation?: TypeAnnotation | null; + computed: boolean; + static: boolean; + } + + interface DeclareClass extends FlowDeclaration { + id: Identifier; + typeParameters?: TypeParameterDeclaration | null; + body: ObjectTypeAnnotation; + extends: InterfaceExtends[]; + } + + interface DeclareFunction extends FlowDeclaration { + id: Identifier; + } + + interface DeclareModule extends FlowDeclaration { + id: Literal | Identifier; + body: BlockStatement; + } + + interface DeclareVariable extends FlowDeclaration { + id: Identifier; + } + + interface FunctionTypeAnnotation extends FlowTypeAnnotation { + params: FunctionTypeParam[]; + returnType: FlowTypeAnnotation; + rest?: FunctionTypeParam | null; + typeParameters?: TypeParameterDeclaration | null; + } + + interface FunctionTypeParam { + name: Identifier; + typeAnnotation: FlowTypeAnnotation; + optional: boolean; + } + + interface GenericTypeAnnotation extends FlowTypeAnnotation { + id: Identifier | QualifiedTypeIdentifier; + typeParameters?: TypeParameterInstantiation | null; + } + + interface InterfaceExtends extends Node { + id: Identifier | QualifiedTypeIdentifier; + typeParameters?: TypeParameterInstantiation | null; + } + + interface InterfaceDeclaration extends FlowDeclaration { + id: Identifier; + typeParameters?: TypeParameterDeclaration | null; + extends: InterfaceExtends[]; + body: ObjectTypeAnnotation; + } + + interface IntersectionTypeAnnotation extends FlowTypeAnnotation { + types: FlowTypeAnnotation[]; + } + + interface MixedTypeAnnotation extends FlowBaseTypeAnnotation {} + + interface NullableTypeAnnotation extends FlowTypeAnnotation { + typeAnnotation: TypeAnnotation; + } + + interface NumberLiteralTypeAnnotation extends FlowLiteralTypeAnnotation {} + + interface NumberTypeAnnotation extends FlowBaseTypeAnnotation {} + + interface StringLiteralTypeAnnotation extends FlowLiteralTypeAnnotation {} + + interface StringTypeAnnotation extends FlowBaseTypeAnnotation {} + + interface TupleTypeAnnotation extends FlowTypeAnnotation { + types: FlowTypeAnnotation[]; + } + + interface TypeofTypeAnnotation extends FlowTypeAnnotation { + argument: FlowTypeAnnotation; + } + + interface TypeAlias extends FlowDeclaration { + id: Identifier; + typeParameters?: TypeParameterDeclaration | null; + right: FlowTypeAnnotation; + } + + interface TypeAnnotation extends Node { + typeAnnotation: FlowTypeAnnotation; + } + + interface TypeCastExpression extends Expression { + expression: Expression; + typeAnnotation: TypeAnnotation; + } + + interface TypeParameterDeclaration extends Node { + params: Identifier[]; + } + + interface TypeParameterInstantiation extends Node { + params: FlowTypeAnnotation[]; + } + + interface ObjectTypeAnnotation extends FlowTypeAnnotation { + properties: ObjectTypeProperty[]; + indexers: ObjectTypeIndexer[]; + callProperties: ObjectTypeCallProperty[]; + } + + interface ObjectTypeCallProperty extends Node { + value: FunctionTypeAnnotation; + static: boolean; + } + + interface ObjectTypeIndexer extends Node { + id: Identifier; + key: FlowTypeAnnotation; + value: FlowTypeAnnotation; + static: boolean; + } + + interface ObjectTypeProperty extends Node { + key: Expression; + value: FlowTypeAnnotation; + optional: boolean; + static: boolean; + } + + interface QualifiedTypeIdentifier extends Node { + qualification: Identifier | QualifiedTypeIdentifier; + id: Identifier; + } + + interface UnionTypeAnnotation extends FlowTypeAnnotation { + types: FlowTypeAnnotation[]; + } + + interface VoidTypeAnnotation extends FlowBaseTypeAnnotation {} +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/index.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/index.d.ts new file mode 100644 index 00000000..2bc66fb6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/index.d.ts @@ -0,0 +1,694 @@ +// This definition file follows a somewhat unusual format. ESTree allows +// runtime type checks based on the `type` parameter. In order to explain this +// to typescript we want to use discriminated union types: +// https://github.com/Microsoft/TypeScript/pull/9163 +// +// For ESTree this is a bit tricky because the high level interfaces like +// Node or Function are pulling double duty. We want to pass common fields down +// to the interfaces that extend them (like Identifier or +// ArrowFunctionExpression), but you can't extend a type union or enforce +// common fields on them. So we've split the high level interfaces into two +// types, a base type which passes down inherited fields, and a type union of +// all types which extend the base type. Only the type union is exported, and +// the union is how other types refer to the collection of inheriting types. +// +// This makes the definitions file here somewhat more difficult to maintain, +// but it has the notable advantage of making ESTree much easier to use as +// an end user. + +export interface BaseNodeWithoutComments { + // Every leaf interface that extends BaseNode must specify a type property. + // The type property should be a string literal. For example, Identifier + // has: `type: "Identifier"` + type: string; + loc?: SourceLocation | null | undefined; + range?: [number, number] | undefined; +} + +export interface BaseNode extends BaseNodeWithoutComments { + leadingComments?: Comment[] | undefined; + trailingComments?: Comment[] | undefined; +} + +export interface NodeMap { + AssignmentProperty: AssignmentProperty; + CatchClause: CatchClause; + Class: Class; + ClassBody: ClassBody; + Expression: Expression; + Function: Function; + Identifier: Identifier; + Literal: Literal; + MethodDefinition: MethodDefinition; + ModuleDeclaration: ModuleDeclaration; + ModuleSpecifier: ModuleSpecifier; + Pattern: Pattern; + PrivateIdentifier: PrivateIdentifier; + Program: Program; + Property: Property; + PropertyDefinition: PropertyDefinition; + SpreadElement: SpreadElement; + Statement: Statement; + Super: Super; + SwitchCase: SwitchCase; + TemplateElement: TemplateElement; + VariableDeclarator: VariableDeclarator; +} + +export type Node = NodeMap[keyof NodeMap]; + +export interface Comment extends BaseNodeWithoutComments { + type: "Line" | "Block"; + value: string; +} + +export interface SourceLocation { + source?: string | null | undefined; + start: Position; + end: Position; +} + +export interface Position { + /** >= 1 */ + line: number; + /** >= 0 */ + column: number; +} + +export interface Program extends BaseNode { + type: "Program"; + sourceType: "script" | "module"; + body: Array; + comments?: Comment[] | undefined; +} + +export interface Directive extends BaseNode { + type: "ExpressionStatement"; + expression: Literal; + directive: string; +} + +export interface BaseFunction extends BaseNode { + params: Pattern[]; + generator?: boolean | undefined; + async?: boolean | undefined; + // The body is either BlockStatement or Expression because arrow functions + // can have a body that's either. FunctionDeclarations and + // FunctionExpressions have only BlockStatement bodies. + body: BlockStatement | Expression; +} + +export type Function = FunctionDeclaration | FunctionExpression | ArrowFunctionExpression; + +export type Statement = + | ExpressionStatement + | BlockStatement + | StaticBlock + | EmptyStatement + | DebuggerStatement + | WithStatement + | ReturnStatement + | LabeledStatement + | BreakStatement + | ContinueStatement + | IfStatement + | SwitchStatement + | ThrowStatement + | TryStatement + | WhileStatement + | DoWhileStatement + | ForStatement + | ForInStatement + | ForOfStatement + | Declaration; + +export interface BaseStatement extends BaseNode {} + +export interface EmptyStatement extends BaseStatement { + type: "EmptyStatement"; +} + +export interface BlockStatement extends BaseStatement { + type: "BlockStatement"; + body: Statement[]; + innerComments?: Comment[] | undefined; +} + +export interface StaticBlock extends Omit { + type: "StaticBlock"; +} + +export interface ExpressionStatement extends BaseStatement { + type: "ExpressionStatement"; + expression: Expression; +} + +export interface IfStatement extends BaseStatement { + type: "IfStatement"; + test: Expression; + consequent: Statement; + alternate?: Statement | null | undefined; +} + +export interface LabeledStatement extends BaseStatement { + type: "LabeledStatement"; + label: Identifier; + body: Statement; +} + +export interface BreakStatement extends BaseStatement { + type: "BreakStatement"; + label?: Identifier | null | undefined; +} + +export interface ContinueStatement extends BaseStatement { + type: "ContinueStatement"; + label?: Identifier | null | undefined; +} + +export interface WithStatement extends BaseStatement { + type: "WithStatement"; + object: Expression; + body: Statement; +} + +export interface SwitchStatement extends BaseStatement { + type: "SwitchStatement"; + discriminant: Expression; + cases: SwitchCase[]; +} + +export interface ReturnStatement extends BaseStatement { + type: "ReturnStatement"; + argument?: Expression | null | undefined; +} + +export interface ThrowStatement extends BaseStatement { + type: "ThrowStatement"; + argument: Expression; +} + +export interface TryStatement extends BaseStatement { + type: "TryStatement"; + block: BlockStatement; + handler?: CatchClause | null | undefined; + finalizer?: BlockStatement | null | undefined; +} + +export interface WhileStatement extends BaseStatement { + type: "WhileStatement"; + test: Expression; + body: Statement; +} + +export interface DoWhileStatement extends BaseStatement { + type: "DoWhileStatement"; + body: Statement; + test: Expression; +} + +export interface ForStatement extends BaseStatement { + type: "ForStatement"; + init?: VariableDeclaration | Expression | null | undefined; + test?: Expression | null | undefined; + update?: Expression | null | undefined; + body: Statement; +} + +export interface BaseForXStatement extends BaseStatement { + left: VariableDeclaration | Pattern; + right: Expression; + body: Statement; +} + +export interface ForInStatement extends BaseForXStatement { + type: "ForInStatement"; +} + +export interface DebuggerStatement extends BaseStatement { + type: "DebuggerStatement"; +} + +export type Declaration = FunctionDeclaration | VariableDeclaration | ClassDeclaration; + +export interface BaseDeclaration extends BaseStatement {} + +export interface MaybeNamedFunctionDeclaration extends BaseFunction, BaseDeclaration { + type: "FunctionDeclaration"; + /** It is null when a function declaration is a part of the `export default function` statement */ + id: Identifier | null; + body: BlockStatement; +} + +export interface FunctionDeclaration extends MaybeNamedFunctionDeclaration { + id: Identifier; +} + +export interface VariableDeclaration extends BaseDeclaration { + type: "VariableDeclaration"; + declarations: VariableDeclarator[]; + kind: "var" | "let" | "const" | "using" | "await using"; +} + +export interface VariableDeclarator extends BaseNode { + type: "VariableDeclarator"; + id: Pattern; + init?: Expression | null | undefined; +} + +export interface ExpressionMap { + ArrayExpression: ArrayExpression; + ArrowFunctionExpression: ArrowFunctionExpression; + AssignmentExpression: AssignmentExpression; + AwaitExpression: AwaitExpression; + BinaryExpression: BinaryExpression; + CallExpression: CallExpression; + ChainExpression: ChainExpression; + ClassExpression: ClassExpression; + ConditionalExpression: ConditionalExpression; + FunctionExpression: FunctionExpression; + Identifier: Identifier; + ImportExpression: ImportExpression; + Literal: Literal; + LogicalExpression: LogicalExpression; + MemberExpression: MemberExpression; + MetaProperty: MetaProperty; + NewExpression: NewExpression; + ObjectExpression: ObjectExpression; + SequenceExpression: SequenceExpression; + TaggedTemplateExpression: TaggedTemplateExpression; + TemplateLiteral: TemplateLiteral; + ThisExpression: ThisExpression; + UnaryExpression: UnaryExpression; + UpdateExpression: UpdateExpression; + YieldExpression: YieldExpression; +} + +export type Expression = ExpressionMap[keyof ExpressionMap]; + +export interface BaseExpression extends BaseNode {} + +export type ChainElement = SimpleCallExpression | MemberExpression; + +export interface ChainExpression extends BaseExpression { + type: "ChainExpression"; + expression: ChainElement; +} + +export interface ThisExpression extends BaseExpression { + type: "ThisExpression"; +} + +export interface ArrayExpression extends BaseExpression { + type: "ArrayExpression"; + elements: Array; +} + +export interface ObjectExpression extends BaseExpression { + type: "ObjectExpression"; + properties: Array; +} + +export interface PrivateIdentifier extends BaseNode { + type: "PrivateIdentifier"; + name: string; +} + +export interface Property extends BaseNode { + type: "Property"; + key: Expression | PrivateIdentifier; + value: Expression | Pattern; // Could be an AssignmentProperty + kind: "init" | "get" | "set"; + method: boolean; + shorthand: boolean; + computed: boolean; +} + +export interface PropertyDefinition extends BaseNode { + type: "PropertyDefinition"; + key: Expression | PrivateIdentifier; + value?: Expression | null | undefined; + computed: boolean; + static: boolean; +} + +export interface FunctionExpression extends BaseFunction, BaseExpression { + id?: Identifier | null | undefined; + type: "FunctionExpression"; + body: BlockStatement; +} + +export interface SequenceExpression extends BaseExpression { + type: "SequenceExpression"; + expressions: Expression[]; +} + +export interface UnaryExpression extends BaseExpression { + type: "UnaryExpression"; + operator: UnaryOperator; + prefix: true; + argument: Expression; +} + +export interface BinaryExpression extends BaseExpression { + type: "BinaryExpression"; + operator: BinaryOperator; + left: Expression | PrivateIdentifier; + right: Expression; +} + +export interface AssignmentExpression extends BaseExpression { + type: "AssignmentExpression"; + operator: AssignmentOperator; + left: Pattern | MemberExpression; + right: Expression; +} + +export interface UpdateExpression extends BaseExpression { + type: "UpdateExpression"; + operator: UpdateOperator; + argument: Expression; + prefix: boolean; +} + +export interface LogicalExpression extends BaseExpression { + type: "LogicalExpression"; + operator: LogicalOperator; + left: Expression; + right: Expression; +} + +export interface ConditionalExpression extends BaseExpression { + type: "ConditionalExpression"; + test: Expression; + alternate: Expression; + consequent: Expression; +} + +export interface BaseCallExpression extends BaseExpression { + callee: Expression | Super; + arguments: Array; +} +export type CallExpression = SimpleCallExpression | NewExpression; + +export interface SimpleCallExpression extends BaseCallExpression { + type: "CallExpression"; + optional: boolean; +} + +export interface NewExpression extends BaseCallExpression { + type: "NewExpression"; +} + +export interface MemberExpression extends BaseExpression, BasePattern { + type: "MemberExpression"; + object: Expression | Super; + property: Expression | PrivateIdentifier; + computed: boolean; + optional: boolean; +} + +export type Pattern = Identifier | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | MemberExpression; + +export interface BasePattern extends BaseNode {} + +export interface SwitchCase extends BaseNode { + type: "SwitchCase"; + test?: Expression | null | undefined; + consequent: Statement[]; +} + +export interface CatchClause extends BaseNode { + type: "CatchClause"; + param: Pattern | null; + body: BlockStatement; +} + +export interface Identifier extends BaseNode, BaseExpression, BasePattern { + type: "Identifier"; + name: string; +} + +export type Literal = SimpleLiteral | RegExpLiteral | BigIntLiteral; + +export interface SimpleLiteral extends BaseNode, BaseExpression { + type: "Literal"; + value: string | boolean | number | null; + raw?: string | undefined; +} + +export interface RegExpLiteral extends BaseNode, BaseExpression { + type: "Literal"; + value?: RegExp | null | undefined; + regex: { + pattern: string; + flags: string; + }; + raw?: string | undefined; +} + +export interface BigIntLiteral extends BaseNode, BaseExpression { + type: "Literal"; + value?: bigint | null | undefined; + bigint: string; + raw?: string | undefined; +} + +export type UnaryOperator = "-" | "+" | "!" | "~" | "typeof" | "void" | "delete"; + +export type BinaryOperator = + | "==" + | "!=" + | "===" + | "!==" + | "<" + | "<=" + | ">" + | ">=" + | "<<" + | ">>" + | ">>>" + | "+" + | "-" + | "*" + | "/" + | "%" + | "**" + | "|" + | "^" + | "&" + | "in" + | "instanceof"; + +export type LogicalOperator = "||" | "&&" | "??"; + +export type AssignmentOperator = + | "=" + | "+=" + | "-=" + | "*=" + | "/=" + | "%=" + | "**=" + | "<<=" + | ">>=" + | ">>>=" + | "|=" + | "^=" + | "&=" + | "||=" + | "&&=" + | "??="; + +export type UpdateOperator = "++" | "--"; + +export interface ForOfStatement extends BaseForXStatement { + type: "ForOfStatement"; + await: boolean; +} + +export interface Super extends BaseNode { + type: "Super"; +} + +export interface SpreadElement extends BaseNode { + type: "SpreadElement"; + argument: Expression; +} + +export interface ArrowFunctionExpression extends BaseExpression, BaseFunction { + type: "ArrowFunctionExpression"; + expression: boolean; + body: BlockStatement | Expression; +} + +export interface YieldExpression extends BaseExpression { + type: "YieldExpression"; + argument?: Expression | null | undefined; + delegate: boolean; +} + +export interface TemplateLiteral extends BaseExpression { + type: "TemplateLiteral"; + quasis: TemplateElement[]; + expressions: Expression[]; +} + +export interface TaggedTemplateExpression extends BaseExpression { + type: "TaggedTemplateExpression"; + tag: Expression; + quasi: TemplateLiteral; +} + +export interface TemplateElement extends BaseNode { + type: "TemplateElement"; + tail: boolean; + value: { + /** It is null when the template literal is tagged and the text has an invalid escape (e.g. - tag`\unicode and \u{55}`) */ + cooked?: string | null | undefined; + raw: string; + }; +} + +export interface AssignmentProperty extends Property { + value: Pattern; + kind: "init"; + method: boolean; // false +} + +export interface ObjectPattern extends BasePattern { + type: "ObjectPattern"; + properties: Array; +} + +export interface ArrayPattern extends BasePattern { + type: "ArrayPattern"; + elements: Array; +} + +export interface RestElement extends BasePattern { + type: "RestElement"; + argument: Pattern; +} + +export interface AssignmentPattern extends BasePattern { + type: "AssignmentPattern"; + left: Pattern; + right: Expression; +} + +export type Class = ClassDeclaration | ClassExpression; +export interface BaseClass extends BaseNode { + superClass?: Expression | null | undefined; + body: ClassBody; +} + +export interface ClassBody extends BaseNode { + type: "ClassBody"; + body: Array; +} + +export interface MethodDefinition extends BaseNode { + type: "MethodDefinition"; + key: Expression | PrivateIdentifier; + value: FunctionExpression; + kind: "constructor" | "method" | "get" | "set"; + computed: boolean; + static: boolean; +} + +export interface MaybeNamedClassDeclaration extends BaseClass, BaseDeclaration { + type: "ClassDeclaration"; + /** It is null when a class declaration is a part of the `export default class` statement */ + id: Identifier | null; +} + +export interface ClassDeclaration extends MaybeNamedClassDeclaration { + id: Identifier; +} + +export interface ClassExpression extends BaseClass, BaseExpression { + type: "ClassExpression"; + id?: Identifier | null | undefined; +} + +export interface MetaProperty extends BaseExpression { + type: "MetaProperty"; + meta: Identifier; + property: Identifier; +} + +export type ModuleDeclaration = + | ImportDeclaration + | ExportNamedDeclaration + | ExportDefaultDeclaration + | ExportAllDeclaration; +export interface BaseModuleDeclaration extends BaseNode {} + +export type ModuleSpecifier = ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier; +export interface BaseModuleSpecifier extends BaseNode { + local: Identifier; +} + +export interface ImportDeclaration extends BaseModuleDeclaration { + type: "ImportDeclaration"; + specifiers: Array; + attributes: ImportAttribute[]; + source: Literal; +} + +export interface ImportSpecifier extends BaseModuleSpecifier { + type: "ImportSpecifier"; + imported: Identifier | Literal; +} + +export interface ImportAttribute extends BaseNode { + type: "ImportAttribute"; + key: Identifier | Literal; + value: Literal; +} + +export interface ImportExpression extends BaseExpression { + type: "ImportExpression"; + source: Expression; + options?: Expression | null | undefined; +} + +export interface ImportDefaultSpecifier extends BaseModuleSpecifier { + type: "ImportDefaultSpecifier"; +} + +export interface ImportNamespaceSpecifier extends BaseModuleSpecifier { + type: "ImportNamespaceSpecifier"; +} + +export interface ExportNamedDeclaration extends BaseModuleDeclaration { + type: "ExportNamedDeclaration"; + declaration?: Declaration | null | undefined; + specifiers: ExportSpecifier[]; + attributes: ImportAttribute[]; + source?: Literal | null | undefined; +} + +export interface ExportSpecifier extends Omit { + type: "ExportSpecifier"; + local: Identifier | Literal; + exported: Identifier | Literal; +} + +export interface ExportDefaultDeclaration extends BaseModuleDeclaration { + type: "ExportDefaultDeclaration"; + declaration: MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | Expression; +} + +export interface ExportAllDeclaration extends BaseModuleDeclaration { + type: "ExportAllDeclaration"; + exported: Identifier | Literal | null; + attributes: ImportAttribute[]; + source: Literal; +} + +export interface AwaitExpression extends BaseExpression { + type: "AwaitExpression"; + argument: Expression; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/package.json new file mode 100644 index 00000000..68c0782c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/estree/package.json @@ -0,0 +1,27 @@ +{ + "name": "@types/estree", + "version": "1.0.8", + "description": "TypeScript definitions for estree", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree", + "license": "MIT", + "contributors": [ + { + "name": "RReverser", + "githubUsername": "RReverser", + "url": "https://github.com/RReverser" + } + ], + "main": "", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/estree" + }, + "scripts": {}, + "dependencies": {}, + "peerDependencies": {}, + "typesPublisherContentHash": "7a167b6e4a4d9f6e9a2cb9fd3fc45c885f89cbdeb44b3e5961bb057a45c082fd", + "typeScriptVersion": "5.1", + "nonNpm": true +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/LICENSE new file mode 100755 index 00000000..9e841e7a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/README.md new file mode 100755 index 00000000..5c20967e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/README.md @@ -0,0 +1,16 @@ +# Installation +> `npm install --save @types/minimatch` + +# Summary +This package contains type definitions for minimatch (https://github.com/isaacs/minimatch). + +# Details +Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/minimatch. + +### Additional Details + * Last updated: Wed, 31 Aug 2022 17:32:44 GMT + * Dependencies: none + * Global values: none + +# Credits +These definitions were written by [vvakame](https://github.com/vvakame), [Shant Marouti](https://github.com/shantmarouti), and [BendingBender](https://github.com/BendingBender). diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/index.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/index.d.ts new file mode 100755 index 00000000..01810802 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/index.d.ts @@ -0,0 +1,300 @@ +// Type definitions for minimatch 5.1 +// Project: https://github.com/isaacs/minimatch +// Definitions by: vvakame +// Shant Marouti +// BendingBender +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/** + * Tests a path against the pattern using the options. + * + * @example + * import minimatch = require("minimatch"); + * + * const isJS = minimatch(file, "*.js", { matchBase: true }); + */ +declare function minimatch(target: string, pattern: string, options?: minimatch.IOptions): boolean; + +declare namespace minimatch { + /** + * Match against the list of files, in the style of fnmatch or glob. + * If nothing is matched, and options.nonull is set, + * then return a list containing the pattern itself. + * + * @example + * import minimatch = require("minimatch"); + * + * const javascripts = minimatch.match(fileList, "*.js", {matchBase: true}); + */ + function match(list: readonly string[], pattern: string, options?: IOptions): string[]; + + /** + * @return A function that tests its supplied argument, suitable for use with `Array.filter`. + * + * @example + * import minimatch = require("minimatch"); + * + * const javascripts = fileList.filter(minimatch.filter("*.js", {matchBase: true})); + */ + function filter( + pattern: string, + options?: IOptions, + ): (element: string, indexed: number, array: readonly string[]) => boolean; + + /** + * Make a regular expression object from the pattern. + */ + function makeRe(pattern: string, options?: IOptions): RegExp | false; + + function defaults(defaultOptions: IOptions): typeof minimatch; + + function braceExpand(pattern: string, options?: IOptions): string[]; + + const sep: string; + const GLOBSTAR: unique symbol; + + interface IOptions { + /** + * Dump a ton of stuff to stderr. + * + * @default false + */ + debug?: boolean | undefined; + + /** + * Do not expand `{a,b}` and `{1..3}` brace sets. + * + * @default false + */ + nobrace?: boolean | undefined; + + /** + * Disable `**` matching against multiple folder names. + * + * @default false + */ + noglobstar?: boolean | undefined; + + /** + * Allow patterns to match filenames starting with a period, + * even if the pattern does not explicitly have a period in that spot. + * + * Note that by default, `'a/**' + '/b'` will **not** match `a/.d/b`, unless `dot` is set. + * + * @default false + */ + dot?: boolean | undefined; + + /** + * Disable "extglob" style patterns like `+(a|b)`. + * + * @default false + */ + noext?: boolean | undefined; + + /** + * Perform a case-insensitive match. + * + * @default false + */ + nocase?: boolean | undefined; + + /** + * When a match is not found by `minimatch.match`, + * return a list containing the pattern itself if this option is set. + * Otherwise, an empty list is returned if there are no matches. + * + * @default false + */ + nonull?: boolean | undefined; + + /** + * If set, then patterns without slashes will be matched + * against the basename of the path if it contains slashes. For example, + * `a?b` would match the path `/xyz/123/acb`, but not `/xyz/acb/123`. + * + * @default false + */ + matchBase?: boolean | undefined; + + /** + * Suppress the behavior of treating `#` at the start of a pattern as a comment. + * + * @default false + */ + nocomment?: boolean | undefined; + + /** + * Suppress the behavior of treating a leading `!` character as negation. + * + * @default false + */ + nonegate?: boolean | undefined; + + /** + * Returns from negate expressions the same as if they were not negated. + * (Ie, true on a hit, false on a miss.) + * + * @default false + */ + flipNegate?: boolean | undefined; + + /** + * Compare a partial path to a pattern. As long as the parts of the path that + * are present are not contradicted by the pattern, it will be treated as a + * match. This is useful in applications where you're walking through a + * folder structure, and don't yet have the full path, but want to ensure that + * you do not walk down paths that can never be a match. + * + * @default false + * + * @example + * import minimatch = require("minimatch"); + * + * minimatch('/a/b', '/a/*' + '/c/d', { partial: true }) // true, might be /a/b/c/d + * minimatch('/a/b', '/**' + '/d', { partial: true }) // true, might be /a/b/.../d + * minimatch('/x/y/z', '/a/**' + '/z', { partial: true }) // false, because x !== a + */ + partial?: boolean; + + /** + * Use `\\` as a path separator _only_, and _never_ as an escape + * character. If set, all `\\` characters are replaced with `/` in + * the pattern. Note that this makes it **impossible** to match + * against paths containing literal glob pattern characters, but + * allows matching with patterns constructed using `path.join()` and + * `path.resolve()` on Windows platforms, mimicking the (buggy!) + * behavior of earlier versions on Windows. Please use with + * caution, and be mindful of the caveat about Windows paths + * + * For legacy reasons, this is also set if + * `options.allowWindowsEscape` is set to the exact value `false`. + * + * @default false + */ + windowsPathsNoEscape?: boolean; + } + + /** + * @deprecated Keep legacy interface to prevent unnecessary breakage. + */ + type IMinimatchStatic = typeof Minimatch; + /** + * @deprecated Keep legacy interface to prevent unnecessary breakage. + */ + type IMinimatch = Minimatch; + + /** + * Create a minimatch object by instantiating the `minimatch.Minimatch` class. + * + * @example + * import { Minimatch } from "minimatch"; + * + * const mm = new Minimatch(pattern, options); + */ + class Minimatch { + constructor(pattern: string, options?: IOptions); + + static defaults(defaultOptions: IOptions): typeof Minimatch; + + /** + * The original pattern the minimatch object represents. + */ + pattern: string; + + /** + * The options supplied to the constructor. + */ + options: IOptions; + + /** + * A 2-dimensional array of regexp or string expressions. Each row in the array corresponds + * to a brace-expanded pattern. Each item in the row corresponds to a single path-part. For + * example, the pattern `{a,b/c}/d` would expand to a set of patterns like: + * + * ``` + * [ [ a, d ] + * , [ b, c, d ] ] + * ``` + * + * If a portion of the pattern doesn't have any "magic" in it (that is, it's something like `"foo"`` + * rather than `fo*o?`), then it will be left as a string rather than converted to a regular expression. + */ + set: Array>; + + /** + * Created by the `makeRe` method. A single regular expression expressing the entire pattern. This is + * useful in cases where you wish to use the pattern somewhat like `fnmatch(3)` with `FNM_PATH` enabled. + */ + regexp: RegExp | false | null; + + /** + * True if the pattern is negated. + */ + negate: boolean; + + /** + * True if the pattern is a comment. + */ + comment: boolean; + + /** + * True if the pattern is `""`. + */ + empty: boolean; + + /** + * True if windows path delimiters shouldn't be interpreted as escape characters. + */ + windowsPathsNoEscape: boolean; + + /** + * True if partial paths should be compared to a pattern. + */ + partial: boolean; + + /** + * Generate the `regexp` member if necessary, and return it. Will return `false` if the pattern is invalid. + */ + makeRe(): RegExp | false; + + /** + * @return `true` if the filename matches the pattern, or `false` otherwise. + */ + match(fname: string, partial?: boolean): boolean; + + /** + * Take a `/`-split filename, and match it against a single row in the `regExpSet`. + * This method is mainly for internal use, but is exposed so that it can be used + * by a glob-walker that needs to avoid excessive filesystem calls. + */ + matchOne(file: readonly string[], pattern: ReadonlyArray, partial: boolean): boolean; + + /** + * @deprecated. For internal use. + */ + debug(): void; + + /** + * @deprecated. For internal use. + */ + make(): void; + + /** + * @deprecated. For internal use. + */ + parseNegate(): void; + + /** + * @deprecated. For internal use. + */ + braceExpand(): string[]; + + /** + * @deprecated. For internal use. + */ + parse(pattern: string, isSub?: boolean): string | false | [string, boolean] | RegExp | typeof GLOBSTAR; + } +} + +export = minimatch; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/package.json new file mode 100755 index 00000000..35799128 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/minimatch/package.json @@ -0,0 +1,35 @@ +{ + "name": "@types/minimatch", + "version": "5.1.2", + "description": "TypeScript definitions for minimatch", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/minimatch", + "license": "MIT", + "contributors": [ + { + "name": "vvakame", + "url": "https://github.com/vvakame", + "githubUsername": "vvakame" + }, + { + "name": "Shant Marouti", + "url": "https://github.com/shantmarouti", + "githubUsername": "shantmarouti" + }, + { + "name": "BendingBender", + "url": "https://github.com/BendingBender", + "githubUsername": "BendingBender" + } + ], + "main": "", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/minimatch" + }, + "scripts": {}, + "dependencies": {}, + "typesPublisherContentHash": "266f2226f04264f59fb2aeb3afc253d311ddd99b4ae8534d2e27f8a1379203e4", + "typeScriptVersion": "4.1" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/LICENSE new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/README.md new file mode 100644 index 00000000..0eb178ed --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/README.md @@ -0,0 +1,15 @@ +# Installation +> `npm install --save @types/node` + +# Summary +This package contains type definitions for node (https://nodejs.org/). + +# Details +Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node/v22. + +### Additional Details + * Last updated: Fri, 06 Mar 2026 00:57:44 GMT + * Dependencies: [undici-types](https://npmjs.com/package/undici-types) + +# Credits +These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [David Junger](https://github.com/touffy), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Sebastian Silbermann](https://github.com/eps1lon), [Wilco Bakker](https://github.com/WilcoBakker), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), [Dmitry Semigradsky](https://github.com/Semigradsky), and [René](https://github.com/Renegade334). diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/assert.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/assert.d.ts new file mode 100644 index 00000000..330d860c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/assert.d.ts @@ -0,0 +1,1078 @@ +/** + * The `node:assert` module provides a set of assertion functions for verifying + * invariants. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/assert.js) + */ +declare module "assert" { + import strict = require("assert/strict"); + /** + * An alias of {@link assert.ok}. + * @since v0.5.9 + * @param value The input that is checked for being truthy. + */ + function assert(value: unknown, message?: string | Error): asserts value; + const kOptions: unique symbol; + namespace assert { + type AssertMethodNames = + | "deepEqual" + | "deepStrictEqual" + | "doesNotMatch" + | "doesNotReject" + | "doesNotThrow" + | "equal" + | "fail" + | "ifError" + | "match" + | "notDeepEqual" + | "notDeepStrictEqual" + | "notEqual" + | "notStrictEqual" + | "ok" + | "partialDeepStrictEqual" + | "rejects" + | "strictEqual" + | "throws"; + interface AssertOptions { + /** + * If set to `'full'`, shows the full diff in assertion errors. + * @default 'simple' + */ + diff?: "simple" | "full" | undefined; + /** + * If set to `true`, non-strict methods behave like their + * corresponding strict methods. + * @default true + */ + strict?: boolean | undefined; + } + interface Assert extends Pick { + readonly [kOptions]: AssertOptions & { strict: false }; + } + interface AssertStrict extends Pick { + readonly [kOptions]: AssertOptions & { strict: true }; + } + /** + * The `Assert` class allows creating independent assertion instances with custom options. + * @since v22.19.0 + */ + var Assert: { + /** + * Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages. + * + * ```js + * const { Assert } = require('node:assert'); + * const assertInstance = new Assert({ diff: 'full' }); + * assertInstance.deepStrictEqual({ a: 1 }, { a: 2 }); + * // Shows a full diff in the error message. + * ``` + * + * **Important**: When destructuring assertion methods from an `Assert` instance, + * the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings). + * The destructured methods will fall back to default behavior instead. + * + * ```js + * const myAssert = new Assert({ diff: 'full' }); + * + * // This works as expected - uses 'full' diff + * myAssert.strictEqual({ a: 1 }, { b: { c: 1 } }); + * + * // This loses the 'full' diff setting - falls back to default 'simple' diff + * const { strictEqual } = myAssert; + * strictEqual({ a: 1 }, { b: { c: 1 } }); + * ``` + * + * When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior + * (diff: 'simple', non-strict mode). + * To maintain custom options when using destructured methods, avoid + * destructuring and call methods directly on the instance. + * @since v22.19.0 + */ + new( + options?: AssertOptions & { strict?: true }, + ): AssertStrict; + new( + options: AssertOptions, + ): Assert; + }; + interface AssertionErrorOptions { + /** + * If provided, the error message is set to this value. + */ + message?: string | undefined; + /** + * The `actual` property on the error instance. + */ + actual?: unknown; + /** + * The `expected` property on the error instance. + */ + expected?: unknown; + /** + * The `operator` property on the error instance. + */ + operator?: string | undefined; + /** + * If provided, the generated stack trace omits frames before this function. + */ + stackStartFn?: Function | undefined; + /** + * If set to `'full'`, shows the full diff in assertion errors. + * @default 'simple' + */ + diff?: "simple" | "full" | undefined; + } + /** + * Indicates the failure of an assertion. All errors thrown by the `node:assert` module will be instances of the `AssertionError` class. + */ + class AssertionError extends Error { + constructor(options: AssertionErrorOptions); + /** + * Set to the `actual` argument for methods such as {@link assert.strictEqual()}. + */ + actual: unknown; + /** + * Set to the `expected` argument for methods such as {@link assert.strictEqual()}. + */ + expected: unknown; + /** + * Indicates if the message was auto-generated (`true`) or not. + */ + generatedMessage: boolean; + /** + * Value is always `ERR_ASSERTION` to show that the error is an assertion error. + */ + code: "ERR_ASSERTION"; + /** + * Set to the passed in operator value. + */ + operator: string; + } + /** + * This feature is deprecated and will be removed in a future version. + * Please consider using alternatives such as the `mock` helper function. + * @since v14.2.0, v12.19.0 + * @deprecated Deprecated + */ + class CallTracker { + /** + * The wrapper function is expected to be called exactly `exact` times. If the + * function has not been called exactly `exact` times when `tracker.verify()` is called, then `tracker.verify()` will throw an + * error. + * + * ```js + * import assert from 'node:assert'; + * + * // Creates call tracker. + * const tracker = new assert.CallTracker(); + * + * function func() {} + * + * // Returns a function that wraps func() that must be called exact times + * // before tracker.verify(). + * const callsfunc = tracker.calls(func); + * ``` + * @since v14.2.0, v12.19.0 + * @param [fn='A no-op function'] + * @param [exact=1] + * @return A function that wraps `fn`. + */ + calls(exact?: number): () => void; + calls any>(fn?: Func, exact?: number): Func; + /** + * Example: + * + * ```js + * import assert from 'node:assert'; + * + * const tracker = new assert.CallTracker(); + * + * function func() {} + * const callsfunc = tracker.calls(func); + * callsfunc(1, 2, 3); + * + * assert.deepStrictEqual(tracker.getCalls(callsfunc), + * [{ thisArg: undefined, arguments: [1, 2, 3] }]); + * ``` + * @since v18.8.0, v16.18.0 + * @return An array with all the calls to a tracked function. + */ + getCalls(fn: Function): CallTrackerCall[]; + /** + * The arrays contains information about the expected and actual number of calls of + * the functions that have not been called the expected number of times. + * + * ```js + * import assert from 'node:assert'; + * + * // Creates call tracker. + * const tracker = new assert.CallTracker(); + * + * function func() {} + * + * // Returns a function that wraps func() that must be called exact times + * // before tracker.verify(). + * const callsfunc = tracker.calls(func, 2); + * + * // Returns an array containing information on callsfunc() + * console.log(tracker.report()); + * // [ + * // { + * // message: 'Expected the func function to be executed 2 time(s) but was + * // executed 0 time(s).', + * // actual: 0, + * // expected: 2, + * // operator: 'func', + * // stack: stack trace + * // } + * // ] + * ``` + * @since v14.2.0, v12.19.0 + * @return An array of objects containing information about the wrapper functions returned by {@link tracker.calls()}. + */ + report(): CallTrackerReportInformation[]; + /** + * Reset calls of the call tracker. If a tracked function is passed as an argument, the calls will be reset for it. + * If no arguments are passed, all tracked functions will be reset. + * + * ```js + * import assert from 'node:assert'; + * + * const tracker = new assert.CallTracker(); + * + * function func() {} + * const callsfunc = tracker.calls(func); + * + * callsfunc(); + * // Tracker was called once + * assert.strictEqual(tracker.getCalls(callsfunc).length, 1); + * + * tracker.reset(callsfunc); + * assert.strictEqual(tracker.getCalls(callsfunc).length, 0); + * ``` + * @since v18.8.0, v16.18.0 + * @param fn a tracked function to reset. + */ + reset(fn?: Function): void; + /** + * Iterates through the list of functions passed to {@link tracker.calls()} and will throw an error for functions that + * have not been called the expected number of times. + * + * ```js + * import assert from 'node:assert'; + * + * // Creates call tracker. + * const tracker = new assert.CallTracker(); + * + * function func() {} + * + * // Returns a function that wraps func() that must be called exact times + * // before tracker.verify(). + * const callsfunc = tracker.calls(func, 2); + * + * callsfunc(); + * + * // Will throw an error since callsfunc() was only called once. + * tracker.verify(); + * ``` + * @since v14.2.0, v12.19.0 + */ + verify(): void; + } + interface CallTrackerCall { + thisArg: object; + arguments: unknown[]; + } + interface CallTrackerReportInformation { + message: string; + /** The actual number of times the function was called. */ + actual: number; + /** The number of times the function was expected to be called. */ + expected: number; + /** The name of the function that is wrapped. */ + operator: string; + /** A stack trace of the function. */ + stack: object; + } + type AssertPredicate = RegExp | (new() => object) | ((thrown: unknown) => boolean) | object | Error; + /** + * Throws an `AssertionError` with the provided error message or a default + * error message. If the `message` parameter is an instance of an `Error` then + * it will be thrown instead of the `AssertionError`. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.fail(); + * // AssertionError [ERR_ASSERTION]: Failed + * + * assert.fail('boom'); + * // AssertionError [ERR_ASSERTION]: boom + * + * assert.fail(new TypeError('need array')); + * // TypeError: need array + * ``` + * + * Using `assert.fail()` with more than two arguments is possible but deprecated. + * See below for further details. + * @since v0.1.21 + * @param [message='Failed'] + */ + function fail(message?: string | Error): never; + /** @deprecated since v10.0.0 - use fail([message]) or other assert functions instead. */ + function fail( + actual: unknown, + expected: unknown, + message?: string | Error, + operator?: string, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + stackStartFn?: Function, + ): never; + /** + * Tests if `value` is truthy. It is equivalent to `assert.equal(!!value, true, message)`. + * + * If `value` is not truthy, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is `undefined`, a default + * error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown instead of the `AssertionError`. + * If no arguments are passed in at all `message` will be set to the string:`` 'No value argument passed to `assert.ok()`' ``. + * + * Be aware that in the `repl` the error message will be different to the one + * thrown in a file! See below for further details. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.ok(true); + * // OK + * assert.ok(1); + * // OK + * + * assert.ok(); + * // AssertionError: No value argument passed to `assert.ok()` + * + * assert.ok(false, 'it\'s false'); + * // AssertionError: it's false + * + * // In the repl: + * assert.ok(typeof 123 === 'string'); + * // AssertionError: false == true + * + * // In a file (e.g. test.js): + * assert.ok(typeof 123 === 'string'); + * // AssertionError: The expression evaluated to a falsy value: + * // + * // assert.ok(typeof 123 === 'string') + * + * assert.ok(false); + * // AssertionError: The expression evaluated to a falsy value: + * // + * // assert.ok(false) + * + * assert.ok(0); + * // AssertionError: The expression evaluated to a falsy value: + * // + * // assert.ok(0) + * ``` + * + * ```js + * import assert from 'node:assert/strict'; + * + * // Using `assert()` works the same: + * assert(0); + * // AssertionError: The expression evaluated to a falsy value: + * // + * // assert(0) + * ``` + * @since v0.1.21 + */ + function ok(value: unknown, message?: string | Error): asserts value; + /** + * **Strict assertion mode** + * + * An alias of {@link strictEqual}. + * + * **Legacy assertion mode** + * + * > Stability: 3 - Legacy: Use {@link strictEqual} instead. + * + * Tests shallow, coercive equality between the `actual` and `expected` parameters + * using the [`==` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality). `NaN` is specially handled + * and treated as being identical if both sides are `NaN`. + * + * ```js + * import assert from 'node:assert'; + * + * assert.equal(1, 1); + * // OK, 1 == 1 + * assert.equal(1, '1'); + * // OK, 1 == '1' + * assert.equal(NaN, NaN); + * // OK + * + * assert.equal(1, 2); + * // AssertionError: 1 == 2 + * assert.equal({ a: { b: 1 } }, { a: { b: 1 } }); + * // AssertionError: { a: { b: 1 } } == { a: { b: 1 } } + * ``` + * + * If the values are not equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default + * error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown instead of the `AssertionError`. + * @since v0.1.21 + */ + function equal(actual: unknown, expected: unknown, message?: string | Error): void; + /** + * **Strict assertion mode** + * + * An alias of {@link notStrictEqual}. + * + * **Legacy assertion mode** + * + * > Stability: 3 - Legacy: Use {@link notStrictEqual} instead. + * + * Tests shallow, coercive inequality with the [`!=` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality). `NaN` is + * specially handled and treated as being identical if both sides are `NaN`. + * + * ```js + * import assert from 'node:assert'; + * + * assert.notEqual(1, 2); + * // OK + * + * assert.notEqual(1, 1); + * // AssertionError: 1 != 1 + * + * assert.notEqual(1, '1'); + * // AssertionError: 1 != '1' + * ``` + * + * If the values are equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default error + * message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown instead of the `AssertionError`. + * @since v0.1.21 + */ + function notEqual(actual: unknown, expected: unknown, message?: string | Error): void; + /** + * **Strict assertion mode** + * + * An alias of {@link deepStrictEqual}. + * + * **Legacy assertion mode** + * + * > Stability: 3 - Legacy: Use {@link deepStrictEqual} instead. + * + * Tests for deep equality between the `actual` and `expected` parameters. Consider + * using {@link deepStrictEqual} instead. {@link deepEqual} can have + * surprising results. + * + * _Deep equality_ means that the enumerable "own" properties of child objects + * are also recursively evaluated by the following rules. + * @since v0.1.21 + */ + function deepEqual(actual: unknown, expected: unknown, message?: string | Error): void; + /** + * **Strict assertion mode** + * + * An alias of {@link notDeepStrictEqual}. + * + * **Legacy assertion mode** + * + * > Stability: 3 - Legacy: Use {@link notDeepStrictEqual} instead. + * + * Tests for any deep inequality. Opposite of {@link deepEqual}. + * + * ```js + * import assert from 'node:assert'; + * + * const obj1 = { + * a: { + * b: 1, + * }, + * }; + * const obj2 = { + * a: { + * b: 2, + * }, + * }; + * const obj3 = { + * a: { + * b: 1, + * }, + * }; + * const obj4 = { __proto__: obj1 }; + * + * assert.notDeepEqual(obj1, obj1); + * // AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } + * + * assert.notDeepEqual(obj1, obj2); + * // OK + * + * assert.notDeepEqual(obj1, obj3); + * // AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } } + * + * assert.notDeepEqual(obj1, obj4); + * // OK + * ``` + * + * If the values are deeply equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default + * error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown + * instead of the `AssertionError`. + * @since v0.1.21 + */ + function notDeepEqual(actual: unknown, expected: unknown, message?: string | Error): void; + /** + * Tests strict equality between the `actual` and `expected` parameters as + * determined by [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.strictEqual(1, 2); + * // AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: + * // + * // 1 !== 2 + * + * assert.strictEqual(1, 1); + * // OK + * + * assert.strictEqual('Hello foobar', 'Hello World!'); + * // AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal: + * // + actual - expected + * // + * // + 'Hello foobar' + * // - 'Hello World!' + * // ^ + * + * const apples = 1; + * const oranges = 2; + * assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`); + * // AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2 + * + * assert.strictEqual(1, '1', new TypeError('Inputs are not identical')); + * // TypeError: Inputs are not identical + * ``` + * + * If the values are not strictly equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a + * default error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown + * instead of the `AssertionError`. + * @since v0.1.21 + */ + function strictEqual(actual: unknown, expected: T, message?: string | Error): asserts actual is T; + /** + * Tests strict inequality between the `actual` and `expected` parameters as + * determined by [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.notStrictEqual(1, 2); + * // OK + * + * assert.notStrictEqual(1, 1); + * // AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to: + * // + * // 1 + * + * assert.notStrictEqual(1, '1'); + * // OK + * ``` + * + * If the values are strictly equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a + * default error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown + * instead of the `AssertionError`. + * @since v0.1.21 + */ + function notStrictEqual(actual: unknown, expected: unknown, message?: string | Error): void; + /** + * Tests for deep equality between the `actual` and `expected` parameters. + * "Deep" equality means that the enumerable "own" properties of child objects + * are recursively evaluated also by the following rules. + * @since v1.2.0 + */ + function deepStrictEqual(actual: unknown, expected: T, message?: string | Error): asserts actual is T; + /** + * Tests for deep strict inequality. Opposite of {@link deepStrictEqual}. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.notDeepStrictEqual({ a: 1 }, { a: '1' }); + * // OK + * ``` + * + * If the values are deeply and strictly equal, an `AssertionError` is thrown + * with a `message` property set equal to the value of the `message` parameter. If + * the `message` parameter is undefined, a default error message is assigned. If + * the `message` parameter is an instance of an `Error` then it will be thrown + * instead of the `AssertionError`. + * @since v1.2.0 + */ + function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string | Error): void; + /** + * Expects the function `fn` to throw an error. + * + * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), + * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), a validation function, + * a validation object where each property will be tested for strict deep equality, + * or an instance of error where each property will be tested for strict deep + * equality including the non-enumerable `message` and `name` properties. When + * using an object, it is also possible to use a regular expression, when + * validating against a string property. See below for examples. + * + * If specified, `message` will be appended to the message provided by the `AssertionError` if the `fn` call fails to throw or in case the error validation + * fails. + * + * Custom validation object/error instance: + * + * ```js + * import assert from 'node:assert/strict'; + * + * const err = new TypeError('Wrong value'); + * err.code = 404; + * err.foo = 'bar'; + * err.info = { + * nested: true, + * baz: 'text', + * }; + * err.reg = /abc/i; + * + * assert.throws( + * () => { + * throw err; + * }, + * { + * name: 'TypeError', + * message: 'Wrong value', + * info: { + * nested: true, + * baz: 'text', + * }, + * // Only properties on the validation object will be tested for. + * // Using nested objects requires all properties to be present. Otherwise + * // the validation is going to fail. + * }, + * ); + * + * // Using regular expressions to validate error properties: + * assert.throws( + * () => { + * throw err; + * }, + * { + * // The `name` and `message` properties are strings and using regular + * // expressions on those will match against the string. If they fail, an + * // error is thrown. + * name: /^TypeError$/, + * message: /Wrong/, + * foo: 'bar', + * info: { + * nested: true, + * // It is not possible to use regular expressions for nested properties! + * baz: 'text', + * }, + * // The `reg` property contains a regular expression and only if the + * // validation object contains an identical regular expression, it is going + * // to pass. + * reg: /abc/i, + * }, + * ); + * + * // Fails due to the different `message` and `name` properties: + * assert.throws( + * () => { + * const otherErr = new Error('Not found'); + * // Copy all enumerable properties from `err` to `otherErr`. + * for (const [key, value] of Object.entries(err)) { + * otherErr[key] = value; + * } + * throw otherErr; + * }, + * // The error's `message` and `name` properties will also be checked when using + * // an error as validation object. + * err, + * ); + * ``` + * + * Validate instanceof using constructor: + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.throws( + * () => { + * throw new Error('Wrong value'); + * }, + * Error, + * ); + * ``` + * + * Validate error message using [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions): + * + * Using a regular expression runs `.toString` on the error object, and will + * therefore also include the error name. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.throws( + * () => { + * throw new Error('Wrong value'); + * }, + * /^Error: Wrong value$/, + * ); + * ``` + * + * Custom error validation: + * + * The function must return `true` to indicate all internal validations passed. + * It will otherwise fail with an `AssertionError`. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.throws( + * () => { + * throw new Error('Wrong value'); + * }, + * (err) => { + * assert(err instanceof Error); + * assert(/value/.test(err)); + * // Avoid returning anything from validation functions besides `true`. + * // Otherwise, it's not clear what part of the validation failed. Instead, + * // throw an error about the specific validation that failed (as done in this + * // example) and add as much helpful debugging information to that error as + * // possible. + * return true; + * }, + * 'unexpected error', + * ); + * ``` + * + * `error` cannot be a string. If a string is provided as the second + * argument, then `error` is assumed to be omitted and the string will be used for `message` instead. This can lead to easy-to-miss mistakes. Using the same + * message as the thrown error message is going to result in an `ERR_AMBIGUOUS_ARGUMENT` error. Please read the example below carefully if using + * a string as the second argument gets considered: + * + * ```js + * import assert from 'node:assert/strict'; + * + * function throwingFirst() { + * throw new Error('First'); + * } + * + * function throwingSecond() { + * throw new Error('Second'); + * } + * + * function notThrowing() {} + * + * // The second argument is a string and the input function threw an Error. + * // The first case will not throw as it does not match for the error message + * // thrown by the input function! + * assert.throws(throwingFirst, 'Second'); + * // In the next example the message has no benefit over the message from the + * // error and since it is not clear if the user intended to actually match + * // against the error message, Node.js throws an `ERR_AMBIGUOUS_ARGUMENT` error. + * assert.throws(throwingSecond, 'Second'); + * // TypeError [ERR_AMBIGUOUS_ARGUMENT] + * + * // The string is only used (as message) in case the function does not throw: + * assert.throws(notThrowing, 'Second'); + * // AssertionError [ERR_ASSERTION]: Missing expected exception: Second + * + * // If it was intended to match for the error message do this instead: + * // It does not throw because the error messages match. + * assert.throws(throwingSecond, /Second$/); + * + * // If the error message does not match, an AssertionError is thrown. + * assert.throws(throwingFirst, /Second$/); + * // AssertionError [ERR_ASSERTION] + * ``` + * + * Due to the confusing error-prone notation, avoid a string as the second + * argument. + * @since v0.1.21 + */ + function throws(block: () => unknown, message?: string | Error): void; + function throws(block: () => unknown, error: AssertPredicate, message?: string | Error): void; + /** + * Asserts that the function `fn` does not throw an error. + * + * Using `assert.doesNotThrow()` is actually not useful because there + * is no benefit in catching an error and then rethrowing it. Instead, consider + * adding a comment next to the specific code path that should not throw and keep + * error messages as expressive as possible. + * + * When `assert.doesNotThrow()` is called, it will immediately call the `fn` function. + * + * If an error is thrown and it is the same type as that specified by the `error` parameter, then an `AssertionError` is thrown. If the error is of a + * different type, or if the `error` parameter is undefined, the error is + * propagated back to the caller. + * + * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), + * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), or a validation + * function. See {@link throws} for more details. + * + * The following, for instance, will throw the `TypeError` because there is no + * matching error type in the assertion: + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.doesNotThrow( + * () => { + * throw new TypeError('Wrong value'); + * }, + * SyntaxError, + * ); + * ``` + * + * However, the following will result in an `AssertionError` with the message + * 'Got unwanted exception...': + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.doesNotThrow( + * () => { + * throw new TypeError('Wrong value'); + * }, + * TypeError, + * ); + * ``` + * + * If an `AssertionError` is thrown and a value is provided for the `message` parameter, the value of `message` will be appended to the `AssertionError` message: + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.doesNotThrow( + * () => { + * throw new TypeError('Wrong value'); + * }, + * /Wrong value/, + * 'Whoops', + * ); + * // Throws: AssertionError: Got unwanted exception: Whoops + * ``` + * @since v0.1.21 + */ + function doesNotThrow(block: () => unknown, message?: string | Error): void; + function doesNotThrow(block: () => unknown, error: AssertPredicate, message?: string | Error): void; + /** + * Throws `value` if `value` is not `undefined` or `null`. This is useful when + * testing the `error` argument in callbacks. The stack trace contains all frames + * from the error passed to `ifError()` including the potential new frames for `ifError()` itself. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.ifError(null); + * // OK + * assert.ifError(0); + * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0 + * assert.ifError('error'); + * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'error' + * assert.ifError(new Error()); + * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Error + * + * // Create some random error frames. + * let err; + * (function errorFrame() { + * err = new Error('test error'); + * })(); + * + * (function ifErrorFrame() { + * assert.ifError(err); + * })(); + * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error + * // at ifErrorFrame + * // at errorFrame + * ``` + * @since v0.1.97 + */ + function ifError(value: unknown): asserts value is null | undefined; + /** + * Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately + * calls the function and awaits the returned promise to complete. It will then + * check that the promise is rejected. + * + * If `asyncFn` is a function and it throws an error synchronously, `assert.rejects()` will return a rejected `Promise` with that error. If the + * function does not return a promise, `assert.rejects()` will return a rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v22.x/api/errors.html#err_invalid_return_value) + * error. In both cases the error handler is skipped. + * + * Besides the async nature to await the completion behaves identically to {@link throws}. + * + * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), + * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), a validation function, + * an object where each property will be tested for, or an instance of error where + * each property will be tested for including the non-enumerable `message` and `name` properties. + * + * If specified, `message` will be the message provided by the `{@link AssertionError}` if the `asyncFn` fails to reject. + * + * ```js + * import assert from 'node:assert/strict'; + * + * await assert.rejects( + * async () => { + * throw new TypeError('Wrong value'); + * }, + * { + * name: 'TypeError', + * message: 'Wrong value', + * }, + * ); + * ``` + * + * ```js + * import assert from 'node:assert/strict'; + * + * await assert.rejects( + * async () => { + * throw new TypeError('Wrong value'); + * }, + * (err) => { + * assert.strictEqual(err.name, 'TypeError'); + * assert.strictEqual(err.message, 'Wrong value'); + * return true; + * }, + * ); + * ``` + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.rejects( + * Promise.reject(new Error('Wrong value')), + * Error, + * ).then(() => { + * // ... + * }); + * ``` + * + * `error` cannot be a string. If a string is provided as the second argument, then `error` is assumed to + * be omitted and the string will be used for `message` instead. This can lead to easy-to-miss mistakes. Please read the + * example in {@link throws} carefully if using a string as the second argument gets considered. + * @since v10.0.0 + */ + function rejects(block: (() => Promise) | Promise, message?: string | Error): Promise; + function rejects( + block: (() => Promise) | Promise, + error: AssertPredicate, + message?: string | Error, + ): Promise; + /** + * Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately + * calls the function and awaits the returned promise to complete. It will then + * check that the promise is not rejected. + * + * If `asyncFn` is a function and it throws an error synchronously, `assert.doesNotReject()` will return a rejected `Promise` with that error. If + * the function does not return a promise, `assert.doesNotReject()` will return a + * rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v22.x/api/errors.html#err_invalid_return_value) error. In both cases + * the error handler is skipped. + * + * Using `assert.doesNotReject()` is actually not useful because there is little + * benefit in catching a rejection and then rejecting it again. Instead, consider + * adding a comment next to the specific code path that should not reject and keep + * error messages as expressive as possible. + * + * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), + * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), or a validation + * function. See {@link throws} for more details. + * + * Besides the async nature to await the completion behaves identically to {@link doesNotThrow}. + * + * ```js + * import assert from 'node:assert/strict'; + * + * await assert.doesNotReject( + * async () => { + * throw new TypeError('Wrong value'); + * }, + * SyntaxError, + * ); + * ``` + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.doesNotReject(Promise.reject(new TypeError('Wrong value'))) + * .then(() => { + * // ... + * }); + * ``` + * @since v10.0.0 + */ + function doesNotReject( + block: (() => Promise) | Promise, + message?: string | Error, + ): Promise; + function doesNotReject( + block: (() => Promise) | Promise, + error: AssertPredicate, + message?: string | Error, + ): Promise; + /** + * Expects the `string` input to match the regular expression. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.match('I will fail', /pass/); + * // AssertionError [ERR_ASSERTION]: The input did not match the regular ... + * + * assert.match(123, /pass/); + * // AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + * + * assert.match('I will pass', /pass/); + * // OK + * ``` + * + * If the values do not match, or if the `string` argument is of another type than `string`, an `{@link AssertionError}` is thrown with a `message` property set equal + * to the value of the `message` parameter. If the `message` parameter is + * undefined, a default error message is assigned. If the `message` parameter is an + * instance of an [Error](https://nodejs.org/docs/latest-v22.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`. + * @since v13.6.0, v12.16.0 + */ + function match(value: string, regExp: RegExp, message?: string | Error): void; + /** + * Expects the `string` input not to match the regular expression. + * + * ```js + * import assert from 'node:assert/strict'; + * + * assert.doesNotMatch('I will fail', /fail/); + * // AssertionError [ERR_ASSERTION]: The input was expected to not match the ... + * + * assert.doesNotMatch(123, /pass/); + * // AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + * + * assert.doesNotMatch('I will pass', /different/); + * // OK + * ``` + * + * If the values do match, or if the `string` argument is of another type than `string`, an `{@link AssertionError}` is thrown with a `message` property set equal + * to the value of the `message` parameter. If the `message` parameter is + * undefined, a default error message is assigned. If the `message` parameter is an + * instance of an [Error](https://nodejs.org/docs/latest-v22.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`. + * @since v13.6.0, v12.16.0 + */ + function doesNotMatch(value: string, regExp: RegExp, message?: string | Error): void; + /** + * Tests for partial deep equality between the `actual` and `expected` parameters. + * "Deep" equality means that the enumerable "own" properties of child objects + * are recursively evaluated also by the following rules. "Partial" equality means + * that only properties that exist on the `expected` parameter are going to be + * compared. + * + * This method always passes the same test cases as `assert.deepStrictEqual()`, + * behaving as a super set of it. + * @since v22.13.0 + */ + function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string | Error): void; + } + namespace assert { + export { strict }; + } + export = assert; +} +declare module "node:assert" { + import assert = require("assert"); + export = assert; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/assert/strict.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/assert/strict.d.ts new file mode 100644 index 00000000..83ce1fe3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/assert/strict.d.ts @@ -0,0 +1,111 @@ +/** + * In strict assertion mode, non-strict methods behave like their corresponding + * strict methods. For example, `assert.deepEqual()` will behave like + * `assert.deepStrictEqual()`. + * + * In strict assertion mode, error messages for objects display a diff. In legacy + * assertion mode, error messages for objects display the objects, often truncated. + * + * To use strict assertion mode: + * + * ```js + * import { strict as assert } from 'node:assert'; + * ``` + * + * ```js + * import assert from 'node:assert/strict'; + * ``` + * + * Example error diff: + * + * ```js + * import { strict as assert } from 'node:assert'; + * + * assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); + * // AssertionError: Expected inputs to be strictly deep-equal: + * // + actual - expected ... Lines skipped + * // + * // [ + * // [ + * // ... + * // 2, + * // + 3 + * // - '3' + * // ], + * // ... + * // 5 + * // ] + * ``` + * + * To deactivate the colors, use the `NO_COLOR` or `NODE_DISABLE_COLORS` + * environment variables. This will also deactivate the colors in the REPL. For + * more on color support in terminal environments, read the tty + * [`getColorDepth()`](https://nodejs.org/docs/latest-v22.x/api/tty.html#writestreamgetcolordepthenv) documentation. + * @since v15.0.0 + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/assert/strict.js) + */ +declare module "assert/strict" { + import { + Assert, + AssertionError, + AssertionErrorOptions, + AssertOptions, + AssertPredicate, + AssertStrict, + CallTracker, + CallTrackerCall, + CallTrackerReportInformation, + deepStrictEqual, + doesNotMatch, + doesNotReject, + doesNotThrow, + fail, + ifError, + match, + notDeepStrictEqual, + notStrictEqual, + ok, + partialDeepStrictEqual, + rejects, + strictEqual, + throws, + } from "node:assert"; + function strict(value: unknown, message?: string | Error): asserts value; + namespace strict { + export { + Assert, + AssertionError, + AssertionErrorOptions, + AssertOptions, + AssertPredicate, + AssertStrict, + CallTracker, + CallTrackerCall, + CallTrackerReportInformation, + deepStrictEqual, + deepStrictEqual as deepEqual, + doesNotMatch, + doesNotReject, + doesNotThrow, + fail, + ifError, + match, + notDeepStrictEqual, + notDeepStrictEqual as notDeepEqual, + notStrictEqual, + notStrictEqual as notEqual, + ok, + partialDeepStrictEqual, + rejects, + strict, + strictEqual, + strictEqual as equal, + throws, + }; + } + export = strict; +} +declare module "node:assert/strict" { + import strict = require("assert/strict"); + export = strict; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/async_hooks.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/async_hooks.d.ts new file mode 100644 index 00000000..01d21d4b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/async_hooks.d.ts @@ -0,0 +1,603 @@ +/** + * We strongly discourage the use of the `async_hooks` API. + * Other APIs that can cover most of its use cases include: + * + * * [`AsyncLocalStorage`](https://nodejs.org/docs/latest-v22.x/api/async_context.html#class-asynclocalstorage) tracks async context + * * [`process.getActiveResourcesInfo()`](https://nodejs.org/docs/latest-v22.x/api/process.html#processgetactiveresourcesinfo) tracks active resources + * + * The `node:async_hooks` module provides an API to track asynchronous resources. + * It can be accessed using: + * + * ```js + * import async_hooks from 'node:async_hooks'; + * ``` + * @experimental + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/async_hooks.js) + */ +declare module "async_hooks" { + /** + * ```js + * import { executionAsyncId } from 'node:async_hooks'; + * import fs from 'node:fs'; + * + * console.log(executionAsyncId()); // 1 - bootstrap + * const path = '.'; + * fs.open(path, 'r', (err, fd) => { + * console.log(executionAsyncId()); // 6 - open() + * }); + * ``` + * + * The ID returned from `executionAsyncId()` is related to execution timing, not + * causality (which is covered by `triggerAsyncId()`): + * + * ```js + * const server = net.createServer((conn) => { + * // Returns the ID of the server, not of the new connection, because the + * // callback runs in the execution scope of the server's MakeCallback(). + * async_hooks.executionAsyncId(); + * + * }).listen(port, () => { + * // Returns the ID of a TickObject (process.nextTick()) because all + * // callbacks passed to .listen() are wrapped in a nextTick(). + * async_hooks.executionAsyncId(); + * }); + * ``` + * + * Promise contexts may not get precise `executionAsyncIds` by default. + * See the section on [promise execution tracking](https://nodejs.org/docs/latest-v22.x/api/async_hooks.html#promise-execution-tracking). + * @since v8.1.0 + * @return The `asyncId` of the current execution context. Useful to track when something calls. + */ + function executionAsyncId(): number; + /** + * Resource objects returned by `executionAsyncResource()` are most often internal + * Node.js handle objects with undocumented APIs. Using any functions or properties + * on the object is likely to crash your application and should be avoided. + * + * Using `executionAsyncResource()` in the top-level execution context will + * return an empty object as there is no handle or request object to use, + * but having an object representing the top-level can be helpful. + * + * ```js + * import { open } from 'node:fs'; + * import { executionAsyncId, executionAsyncResource } from 'node:async_hooks'; + * + * console.log(executionAsyncId(), executionAsyncResource()); // 1 {} + * open(new URL(import.meta.url), 'r', (err, fd) => { + * console.log(executionAsyncId(), executionAsyncResource()); // 7 FSReqWrap + * }); + * ``` + * + * This can be used to implement continuation local storage without the + * use of a tracking `Map` to store the metadata: + * + * ```js + * import { createServer } from 'node:http'; + * import { + * executionAsyncId, + * executionAsyncResource, + * createHook, + * } from 'node:async_hooks'; + * const sym = Symbol('state'); // Private symbol to avoid pollution + * + * createHook({ + * init(asyncId, type, triggerAsyncId, resource) { + * const cr = executionAsyncResource(); + * if (cr) { + * resource[sym] = cr[sym]; + * } + * }, + * }).enable(); + * + * const server = createServer((req, res) => { + * executionAsyncResource()[sym] = { state: req.url }; + * setTimeout(function() { + * res.end(JSON.stringify(executionAsyncResource()[sym])); + * }, 100); + * }).listen(3000); + * ``` + * @since v13.9.0, v12.17.0 + * @return The resource representing the current execution. Useful to store data within the resource. + */ + function executionAsyncResource(): object; + /** + * ```js + * const server = net.createServer((conn) => { + * // The resource that caused (or triggered) this callback to be called + * // was that of the new connection. Thus the return value of triggerAsyncId() + * // is the asyncId of "conn". + * async_hooks.triggerAsyncId(); + * + * }).listen(port, () => { + * // Even though all callbacks passed to .listen() are wrapped in a nextTick() + * // the callback itself exists because the call to the server's .listen() + * // was made. So the return value would be the ID of the server. + * async_hooks.triggerAsyncId(); + * }); + * ``` + * + * Promise contexts may not get valid `triggerAsyncId`s by default. See + * the section on [promise execution tracking](https://nodejs.org/docs/latest-v22.x/api/async_hooks.html#promise-execution-tracking). + * @return The ID of the resource responsible for calling the callback that is currently being executed. + */ + function triggerAsyncId(): number; + interface HookCallbacks { + /** + * Called when a class is constructed that has the possibility to emit an asynchronous event. + * @param asyncId A unique ID for the async resource + * @param type The type of the async resource + * @param triggerAsyncId The unique ID of the async resource in whose execution context this async resource was created + * @param resource Reference to the resource representing the async operation, needs to be released during destroy + */ + init?(asyncId: number, type: string, triggerAsyncId: number, resource: object): void; + /** + * When an asynchronous operation is initiated or completes a callback is called to notify the user. + * The before callback is called just before said callback is executed. + * @param asyncId the unique identifier assigned to the resource about to execute the callback. + */ + before?(asyncId: number): void; + /** + * Called immediately after the callback specified in `before` is completed. + * + * If an uncaught exception occurs during execution of the callback, then `after` will run after the `'uncaughtException'` event is emitted or a `domain`'s handler runs. + * @param asyncId the unique identifier assigned to the resource which has executed the callback. + */ + after?(asyncId: number): void; + /** + * Called when a promise has resolve() called. This may not be in the same execution id + * as the promise itself. + * @param asyncId the unique id for the promise that was resolve()d. + */ + promiseResolve?(asyncId: number): void; + /** + * Called after the resource corresponding to asyncId is destroyed + * @param asyncId a unique ID for the async resource + */ + destroy?(asyncId: number): void; + } + interface AsyncHook { + /** + * Enable the callbacks for a given AsyncHook instance. If no callbacks are provided enabling is a noop. + */ + enable(): this; + /** + * Disable the callbacks for a given AsyncHook instance from the global pool of AsyncHook callbacks to be executed. Once a hook has been disabled it will not be called again until enabled. + */ + disable(): this; + } + /** + * Registers functions to be called for different lifetime events of each async + * operation. + * + * The callbacks `init()`/`before()`/`after()`/`destroy()` are called for the + * respective asynchronous event during a resource's lifetime. + * + * All callbacks are optional. For example, if only resource cleanup needs to + * be tracked, then only the `destroy` callback needs to be passed. The + * specifics of all functions that can be passed to `callbacks` is in the `Hook Callbacks` section. + * + * ```js + * import { createHook } from 'node:async_hooks'; + * + * const asyncHook = createHook({ + * init(asyncId, type, triggerAsyncId, resource) { }, + * destroy(asyncId) { }, + * }); + * ``` + * + * The callbacks will be inherited via the prototype chain: + * + * ```js + * class MyAsyncCallbacks { + * init(asyncId, type, triggerAsyncId, resource) { } + * destroy(asyncId) {} + * } + * + * class MyAddedCallbacks extends MyAsyncCallbacks { + * before(asyncId) { } + * after(asyncId) { } + * } + * + * const asyncHook = async_hooks.createHook(new MyAddedCallbacks()); + * ``` + * + * Because promises are asynchronous resources whose lifecycle is tracked + * via the async hooks mechanism, the `init()`, `before()`, `after()`, and`destroy()` callbacks _must not_ be async functions that return promises. + * @since v8.1.0 + * @param callbacks The `Hook Callbacks` to register + * @return Instance used for disabling and enabling hooks + */ + function createHook(callbacks: HookCallbacks): AsyncHook; + interface AsyncResourceOptions { + /** + * The ID of the execution context that created this async event. + * @default executionAsyncId() + */ + triggerAsyncId?: number | undefined; + /** + * Disables automatic `emitDestroy` when the object is garbage collected. + * This usually does not need to be set (even if `emitDestroy` is called + * manually), unless the resource's `asyncId` is retrieved and the + * sensitive API's `emitDestroy` is called with it. + * @default false + */ + requireManualDestroy?: boolean | undefined; + } + /** + * The class `AsyncResource` is designed to be extended by the embedder's async + * resources. Using this, users can easily trigger the lifetime events of their + * own resources. + * + * The `init` hook will trigger when an `AsyncResource` is instantiated. + * + * The following is an overview of the `AsyncResource` API. + * + * ```js + * import { AsyncResource, executionAsyncId } from 'node:async_hooks'; + * + * // AsyncResource() is meant to be extended. Instantiating a + * // new AsyncResource() also triggers init. If triggerAsyncId is omitted then + * // async_hook.executionAsyncId() is used. + * const asyncResource = new AsyncResource( + * type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false }, + * ); + * + * // Run a function in the execution context of the resource. This will + * // * establish the context of the resource + * // * trigger the AsyncHooks before callbacks + * // * call the provided function `fn` with the supplied arguments + * // * trigger the AsyncHooks after callbacks + * // * restore the original execution context + * asyncResource.runInAsyncScope(fn, thisArg, ...args); + * + * // Call AsyncHooks destroy callbacks. + * asyncResource.emitDestroy(); + * + * // Return the unique ID assigned to the AsyncResource instance. + * asyncResource.asyncId(); + * + * // Return the trigger ID for the AsyncResource instance. + * asyncResource.triggerAsyncId(); + * ``` + */ + class AsyncResource { + /** + * AsyncResource() is meant to be extended. Instantiating a + * new AsyncResource() also triggers init. If triggerAsyncId is omitted then + * async_hook.executionAsyncId() is used. + * @param type The type of async event. + * @param triggerAsyncId The ID of the execution context that created + * this async event (default: `executionAsyncId()`), or an + * AsyncResourceOptions object (since v9.3.0) + */ + constructor(type: string, triggerAsyncId?: number | AsyncResourceOptions); + /** + * Binds the given function to the current execution context. + * @since v14.8.0, v12.19.0 + * @param fn The function to bind to the current execution context. + * @param type An optional name to associate with the underlying `AsyncResource`. + */ + static bind any, ThisArg>( + fn: Func, + type?: string, + thisArg?: ThisArg, + ): Func; + /** + * Binds the given function to execute to this `AsyncResource`'s scope. + * @since v14.8.0, v12.19.0 + * @param fn The function to bind to the current `AsyncResource`. + */ + bind any>(fn: Func): Func; + /** + * Call the provided function with the provided arguments in the execution context + * of the async resource. This will establish the context, trigger the AsyncHooks + * before callbacks, call the function, trigger the AsyncHooks after callbacks, and + * then restore the original execution context. + * @since v9.6.0 + * @param fn The function to call in the execution context of this async resource. + * @param thisArg The receiver to be used for the function call. + * @param args Optional arguments to pass to the function. + */ + runInAsyncScope( + fn: (this: This, ...args: any[]) => Result, + thisArg?: This, + ...args: any[] + ): Result; + /** + * Call all `destroy` hooks. This should only ever be called once. An error will + * be thrown if it is called more than once. This **must** be manually called. If + * the resource is left to be collected by the GC then the `destroy` hooks will + * never be called. + * @return A reference to `asyncResource`. + */ + emitDestroy(): this; + /** + * @return The unique `asyncId` assigned to the resource. + */ + asyncId(): number; + /** + * @return The same `triggerAsyncId` that is passed to the `AsyncResource` constructor. + */ + triggerAsyncId(): number; + } + /** + * This class creates stores that stay coherent through asynchronous operations. + * + * While you can create your own implementation on top of the `node:async_hooks` module, `AsyncLocalStorage` should be preferred as it is a performant and memory + * safe implementation that involves significant optimizations that are non-obvious + * to implement. + * + * The following example uses `AsyncLocalStorage` to build a simple logger + * that assigns IDs to incoming HTTP requests and includes them in messages + * logged within each request. + * + * ```js + * import http from 'node:http'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const asyncLocalStorage = new AsyncLocalStorage(); + * + * function logWithId(msg) { + * const id = asyncLocalStorage.getStore(); + * console.log(`${id !== undefined ? id : '-'}:`, msg); + * } + * + * let idSeq = 0; + * http.createServer((req, res) => { + * asyncLocalStorage.run(idSeq++, () => { + * logWithId('start'); + * // Imagine any chain of async operations here + * setImmediate(() => { + * logWithId('finish'); + * res.end(); + * }); + * }); + * }).listen(8080); + * + * http.get('http://localhost:8080'); + * http.get('http://localhost:8080'); + * // Prints: + * // 0: start + * // 0: finish + * // 1: start + * // 1: finish + * ``` + * + * Each instance of `AsyncLocalStorage` maintains an independent storage context. + * Multiple instances can safely exist simultaneously without risk of interfering + * with each other's data. + * @since v13.10.0, v12.17.0 + */ + class AsyncLocalStorage { + /** + * Binds the given function to the current execution context. + * @since v19.8.0 + * @param fn The function to bind to the current execution context. + * @return A new function that calls `fn` within the captured execution context. + */ + static bind any>(fn: Func): Func; + /** + * Captures the current execution context and returns a function that accepts a + * function as an argument. Whenever the returned function is called, it + * calls the function passed to it within the captured context. + * + * ```js + * const asyncLocalStorage = new AsyncLocalStorage(); + * const runInAsyncScope = asyncLocalStorage.run(123, () => AsyncLocalStorage.snapshot()); + * const result = asyncLocalStorage.run(321, () => runInAsyncScope(() => asyncLocalStorage.getStore())); + * console.log(result); // returns 123 + * ``` + * + * AsyncLocalStorage.snapshot() can replace the use of AsyncResource for simple + * async context tracking purposes, for example: + * + * ```js + * class Foo { + * #runInAsyncScope = AsyncLocalStorage.snapshot(); + * + * get() { return this.#runInAsyncScope(() => asyncLocalStorage.getStore()); } + * } + * + * const foo = asyncLocalStorage.run(123, () => new Foo()); + * console.log(asyncLocalStorage.run(321, () => foo.get())); // returns 123 + * ``` + * @since v19.8.0 + * @return A new function with the signature `(fn: (...args) : R, ...args) : R`. + */ + static snapshot(): (fn: (...args: TArgs) => R, ...args: TArgs) => R; + /** + * Disables the instance of `AsyncLocalStorage`. All subsequent calls + * to `asyncLocalStorage.getStore()` will return `undefined` until `asyncLocalStorage.run()` or `asyncLocalStorage.enterWith()` is called again. + * + * When calling `asyncLocalStorage.disable()`, all current contexts linked to the + * instance will be exited. + * + * Calling `asyncLocalStorage.disable()` is required before the `asyncLocalStorage` can be garbage collected. This does not apply to stores + * provided by the `asyncLocalStorage`, as those objects are garbage collected + * along with the corresponding async resources. + * + * Use this method when the `asyncLocalStorage` is not in use anymore + * in the current process. + * @since v13.10.0, v12.17.0 + * @experimental + */ + disable(): void; + /** + * Returns the current store. + * If called outside of an asynchronous context initialized by + * calling `asyncLocalStorage.run()` or `asyncLocalStorage.enterWith()`, it + * returns `undefined`. + * @since v13.10.0, v12.17.0 + */ + getStore(): T | undefined; + /** + * Runs a function synchronously within a context and returns its + * return value. The store is not accessible outside of the callback function. + * The store is accessible to any asynchronous operations created within the + * callback. + * + * The optional `args` are passed to the callback function. + * + * If the callback function throws an error, the error is thrown by `run()` too. + * The stacktrace is not impacted by this call and the context is exited. + * + * Example: + * + * ```js + * const store = { id: 2 }; + * try { + * asyncLocalStorage.run(store, () => { + * asyncLocalStorage.getStore(); // Returns the store object + * setTimeout(() => { + * asyncLocalStorage.getStore(); // Returns the store object + * }, 200); + * throw new Error(); + * }); + * } catch (e) { + * asyncLocalStorage.getStore(); // Returns undefined + * // The error will be caught here + * } + * ``` + * @since v13.10.0, v12.17.0 + */ + run(store: T, callback: () => R): R; + run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; + /** + * Runs a function synchronously outside of a context and returns its + * return value. The store is not accessible within the callback function or + * the asynchronous operations created within the callback. Any `getStore()` call done within the callback function will always return `undefined`. + * + * The optional `args` are passed to the callback function. + * + * If the callback function throws an error, the error is thrown by `exit()` too. + * The stacktrace is not impacted by this call and the context is re-entered. + * + * Example: + * + * ```js + * // Within a call to run + * try { + * asyncLocalStorage.getStore(); // Returns the store object or value + * asyncLocalStorage.exit(() => { + * asyncLocalStorage.getStore(); // Returns undefined + * throw new Error(); + * }); + * } catch (e) { + * asyncLocalStorage.getStore(); // Returns the same object or value + * // The error will be caught here + * } + * ``` + * @since v13.10.0, v12.17.0 + * @experimental + */ + exit(callback: (...args: TArgs) => R, ...args: TArgs): R; + /** + * Transitions into the context for the remainder of the current + * synchronous execution and then persists the store through any following + * asynchronous calls. + * + * Example: + * + * ```js + * const store = { id: 1 }; + * // Replaces previous store with the given store object + * asyncLocalStorage.enterWith(store); + * asyncLocalStorage.getStore(); // Returns the store object + * someAsyncOperation(() => { + * asyncLocalStorage.getStore(); // Returns the same object + * }); + * ``` + * + * This transition will continue for the _entire_ synchronous execution. + * This means that if, for example, the context is entered within an event + * handler subsequent event handlers will also run within that context unless + * specifically bound to another context with an `AsyncResource`. That is why `run()` should be preferred over `enterWith()` unless there are strong reasons + * to use the latter method. + * + * ```js + * const store = { id: 1 }; + * + * emitter.on('my-event', () => { + * asyncLocalStorage.enterWith(store); + * }); + * emitter.on('my-event', () => { + * asyncLocalStorage.getStore(); // Returns the same object + * }); + * + * asyncLocalStorage.getStore(); // Returns undefined + * emitter.emit('my-event'); + * asyncLocalStorage.getStore(); // Returns the same object + * ``` + * @since v13.11.0, v12.17.0 + * @experimental + */ + enterWith(store: T): void; + } + /** + * @since v17.2.0, v16.14.0 + * @return A map of provider types to the corresponding numeric id. + * This map contains all the event types that might be emitted by the `async_hooks.init()` event. + */ + namespace asyncWrapProviders { + const NONE: number; + const DIRHANDLE: number; + const DNSCHANNEL: number; + const ELDHISTOGRAM: number; + const FILEHANDLE: number; + const FILEHANDLECLOSEREQ: number; + const FIXEDSIZEBLOBCOPY: number; + const FSEVENTWRAP: number; + const FSREQCALLBACK: number; + const FSREQPROMISE: number; + const GETADDRINFOREQWRAP: number; + const GETNAMEINFOREQWRAP: number; + const HEAPSNAPSHOT: number; + const HTTP2SESSION: number; + const HTTP2STREAM: number; + const HTTP2PING: number; + const HTTP2SETTINGS: number; + const HTTPINCOMINGMESSAGE: number; + const HTTPCLIENTREQUEST: number; + const JSSTREAM: number; + const JSUDPWRAP: number; + const MESSAGEPORT: number; + const PIPECONNECTWRAP: number; + const PIPESERVERWRAP: number; + const PIPEWRAP: number; + const PROCESSWRAP: number; + const PROMISE: number; + const QUERYWRAP: number; + const SHUTDOWNWRAP: number; + const SIGNALWRAP: number; + const STATWATCHER: number; + const STREAMPIPE: number; + const TCPCONNECTWRAP: number; + const TCPSERVERWRAP: number; + const TCPWRAP: number; + const TTYWRAP: number; + const UDPSENDWRAP: number; + const UDPWRAP: number; + const SIGINTWATCHDOG: number; + const WORKER: number; + const WORKERHEAPSNAPSHOT: number; + const WRITEWRAP: number; + const ZLIB: number; + const CHECKPRIMEREQUEST: number; + const PBKDF2REQUEST: number; + const KEYPAIRGENREQUEST: number; + const KEYGENREQUEST: number; + const KEYEXPORTREQUEST: number; + const CIPHERREQUEST: number; + const DERIVEBITSREQUEST: number; + const HASHREQUEST: number; + const RANDOMBYTESREQUEST: number; + const RANDOMPRIMEREQUEST: number; + const SCRYPTREQUEST: number; + const SIGNREQUEST: number; + const TLSWRAP: number; + const VERIFYREQUEST: number; + } +} +declare module "node:async_hooks" { + export * from "async_hooks"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/buffer.buffer.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/buffer.buffer.d.ts new file mode 100644 index 00000000..8823deeb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/buffer.buffer.d.ts @@ -0,0 +1,472 @@ +declare module "buffer" { + type ImplicitArrayBuffer> = T extends + { valueOf(): infer V extends ArrayBufferLike } ? V : T; + global { + interface BufferConstructor { + // see buffer.d.ts for implementation shared with all TypeScript versions + + /** + * Allocates a new buffer containing the given {str}. + * + * @param str String to store in buffer. + * @param encoding encoding to use, optional. Default is 'utf8' + * @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. + */ + new(str: string, encoding?: BufferEncoding): Buffer; + /** + * Allocates a new buffer of {size} octets. + * + * @param size count of octets to allocate. + * @deprecated since v10.0.0 - Use `Buffer.alloc()` instead (also see `Buffer.allocUnsafe()`). + */ + new(size: number): Buffer; + /** + * Allocates a new buffer containing the given {array} of octets. + * + * @param array The octets to store. + * @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. + */ + new(array: ArrayLike): Buffer; + /** + * Produces a Buffer backed by the same allocated memory as + * the given {ArrayBuffer}/{SharedArrayBuffer}. + * + * @param arrayBuffer The ArrayBuffer with which to share memory. + * @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. + */ + new(arrayBuffer: TArrayBuffer): Buffer; + /** + * Allocates a new `Buffer` using an `array` of bytes in the range `0` – `255`. + * Array entries outside that range will be truncated to fit into it. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Creates a new Buffer containing the UTF-8 bytes of the string 'buffer'. + * const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); + * ``` + * + * If `array` is an `Array`-like object (that is, one with a `length` property of + * type `number`), it is treated as if it is an array, unless it is a `Buffer` or + * a `Uint8Array`. This means all other `TypedArray` variants get treated as an + * `Array`. To create a `Buffer` from the bytes backing a `TypedArray`, use + * `Buffer.copyBytesFrom()`. + * + * A `TypeError` will be thrown if `array` is not an `Array` or another type + * appropriate for `Buffer.from()` variants. + * + * `Buffer.from(array)` and `Buffer.from(string)` may also use the internal + * `Buffer` pool like `Buffer.allocUnsafe()` does. + * @since v5.10.0 + */ + from(array: WithImplicitCoercion>): Buffer; + /** + * This creates a view of the `ArrayBuffer` without copying the underlying + * memory. For example, when passed a reference to the `.buffer` property of a + * `TypedArray` instance, the newly created `Buffer` will share the same + * allocated memory as the `TypedArray`'s underlying `ArrayBuffer`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const arr = new Uint16Array(2); + * + * arr[0] = 5000; + * arr[1] = 4000; + * + * // Shares memory with `arr`. + * const buf = Buffer.from(arr.buffer); + * + * console.log(buf); + * // Prints: + * + * // Changing the original Uint16Array changes the Buffer also. + * arr[1] = 6000; + * + * console.log(buf); + * // Prints: + * ``` + * + * The optional `byteOffset` and `length` arguments specify a memory range within + * the `arrayBuffer` that will be shared by the `Buffer`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const ab = new ArrayBuffer(10); + * const buf = Buffer.from(ab, 0, 2); + * + * console.log(buf.length); + * // Prints: 2 + * ``` + * + * A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer` or a + * `SharedArrayBuffer` or another type appropriate for `Buffer.from()` + * variants. + * + * It is important to remember that a backing `ArrayBuffer` can cover a range + * of memory that extends beyond the bounds of a `TypedArray` view. A new + * `Buffer` created using the `buffer` property of a `TypedArray` may extend + * beyond the range of the `TypedArray`: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const arrA = Uint8Array.from([0x63, 0x64, 0x65, 0x66]); // 4 elements + * const arrB = new Uint8Array(arrA.buffer, 1, 2); // 2 elements + * console.log(arrA.buffer === arrB.buffer); // true + * + * const buf = Buffer.from(arrB.buffer); + * console.log(buf); + * // Prints: + * ``` + * @since v5.10.0 + * @param arrayBuffer An `ArrayBuffer`, `SharedArrayBuffer`, for example the + * `.buffer` property of a `TypedArray`. + * @param byteOffset Index of first byte to expose. **Default:** `0`. + * @param length Number of bytes to expose. **Default:** + * `arrayBuffer.byteLength - byteOffset`. + */ + from>( + arrayBuffer: TArrayBuffer, + byteOffset?: number, + length?: number, + ): Buffer>; + /** + * Creates a new `Buffer` containing `string`. The `encoding` parameter identifies + * the character encoding to be used when converting `string` into bytes. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('this is a tést'); + * const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex'); + * + * console.log(buf1.toString()); + * // Prints: this is a tést + * console.log(buf2.toString()); + * // Prints: this is a tést + * console.log(buf1.toString('latin1')); + * // Prints: this is a tést + * ``` + * + * A `TypeError` will be thrown if `string` is not a string or another type + * appropriate for `Buffer.from()` variants. + * + * `Buffer.from(string)` may also use the internal `Buffer` pool like + * `Buffer.allocUnsafe()` does. + * @since v5.10.0 + * @param string A string to encode. + * @param encoding The encoding of `string`. **Default:** `'utf8'`. + */ + from(string: WithImplicitCoercion, encoding?: BufferEncoding): Buffer; + from(arrayOrString: WithImplicitCoercion | string>): Buffer; + /** + * Creates a new Buffer using the passed {data} + * @param values to create a new Buffer + */ + of(...items: number[]): Buffer; + /** + * Returns a new `Buffer` which is the result of concatenating all the `Buffer` instances in the `list` together. + * + * If the list has no items, or if the `totalLength` is 0, then a new zero-length `Buffer` is returned. + * + * If `totalLength` is not provided, it is calculated from the `Buffer` instances + * in `list` by adding their lengths. + * + * If `totalLength` is provided, it is coerced to an unsigned integer. If the + * combined length of the `Buffer`s in `list` exceeds `totalLength`, the result is + * truncated to `totalLength`. If the combined length of the `Buffer`s in `list` is + * less than `totalLength`, the remaining space is filled with zeros. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Create a single `Buffer` from a list of three `Buffer` instances. + * + * const buf1 = Buffer.alloc(10); + * const buf2 = Buffer.alloc(14); + * const buf3 = Buffer.alloc(18); + * const totalLength = buf1.length + buf2.length + buf3.length; + * + * console.log(totalLength); + * // Prints: 42 + * + * const bufA = Buffer.concat([buf1, buf2, buf3], totalLength); + * + * console.log(bufA); + * // Prints: + * console.log(bufA.length); + * // Prints: 42 + * ``` + * + * `Buffer.concat()` may also use the internal `Buffer` pool like `Buffer.allocUnsafe()` does. + * @since v0.7.11 + * @param list List of `Buffer` or {@link Uint8Array} instances to concatenate. + * @param totalLength Total length of the `Buffer` instances in `list` when concatenated. + */ + concat(list: readonly Uint8Array[], totalLength?: number): Buffer; + /** + * Copies the underlying memory of `view` into a new `Buffer`. + * + * ```js + * const u16 = new Uint16Array([0, 0xffff]); + * const buf = Buffer.copyBytesFrom(u16, 1, 1); + * u16[1] = 0; + * console.log(buf.length); // 2 + * console.log(buf[0]); // 255 + * console.log(buf[1]); // 255 + * ``` + * @since v19.8.0 + * @param view The {TypedArray} to copy. + * @param [offset=0] The starting offset within `view`. + * @param [length=view.length - offset] The number of elements from `view` to copy. + */ + copyBytesFrom(view: NodeJS.TypedArray, offset?: number, length?: number): Buffer; + /** + * Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the`Buffer` will be zero-filled. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(5); + * + * console.log(buf); + * // Prints: + * ``` + * + * If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. + * + * If `fill` is specified, the allocated `Buffer` will be initialized by calling `buf.fill(fill)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(5, 'a'); + * + * console.log(buf); + * // Prints: + * ``` + * + * If both `fill` and `encoding` are specified, the allocated `Buffer` will be + * initialized by calling `buf.fill(fill, encoding)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64'); + * + * console.log(buf); + * // Prints: + * ``` + * + * Calling `Buffer.alloc()` can be measurably slower than the alternative `Buffer.allocUnsafe()` but ensures that the newly created `Buffer` instance + * contents will never contain sensitive data from previous allocations, including + * data that might not have been allocated for `Buffer`s. + * + * A `TypeError` will be thrown if `size` is not a number. + * @since v5.10.0 + * @param size The desired length of the new `Buffer`. + * @param [fill=0] A value to pre-fill the new `Buffer` with. + * @param [encoding='utf8'] If `fill` is a string, this is its encoding. + */ + alloc(size: number, fill?: string | Uint8Array | number, encoding?: BufferEncoding): Buffer; + /** + * Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. + * + * The underlying memory for `Buffer` instances created in this way is _not_ + * _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `Buffer.alloc()` instead to initialize`Buffer` instances with zeroes. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(10); + * + * console.log(buf); + * // Prints (contents may vary): + * + * buf.fill(0); + * + * console.log(buf); + * // Prints: + * ``` + * + * A `TypeError` will be thrown if `size` is not a number. + * + * The `Buffer` module pre-allocates an internal `Buffer` instance of + * size `Buffer.poolSize` that is used as a pool for the fast allocation of new `Buffer` instances created using `Buffer.allocUnsafe()`, `Buffer.from(array)`, + * and `Buffer.concat()` only when `size` is less than `Buffer.poolSize >>> 1` (floor of `Buffer.poolSize` divided by two). + * + * Use of this pre-allocated internal memory pool is a key difference between + * calling `Buffer.alloc(size, fill)` vs. `Buffer.allocUnsafe(size).fill(fill)`. + * Specifically, `Buffer.alloc(size, fill)` will _never_ use the internal `Buffer`pool, while `Buffer.allocUnsafe(size).fill(fill)`_will_ use the internal`Buffer` pool if `size` is less + * than or equal to half `Buffer.poolSize`. The + * difference is subtle but can be important when an application requires the + * additional performance that `Buffer.allocUnsafe()` provides. + * @since v5.10.0 + * @param size The desired length of the new `Buffer`. + */ + allocUnsafe(size: number): Buffer; + /** + * Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. A zero-length `Buffer` is created if + * `size` is 0. + * + * The underlying memory for `Buffer` instances created in this way is _not_ + * _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `buf.fill(0)` to initialize + * such `Buffer` instances with zeroes. + * + * When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances, + * allocations under 4 KiB are sliced from a single pre-allocated `Buffer`. This + * allows applications to avoid the garbage collection overhead of creating many + * individually allocated `Buffer` instances. This approach improves both + * performance and memory usage by eliminating the need to track and clean up as + * many individual `ArrayBuffer` objects. + * + * However, in the case where a developer may need to retain a small chunk of + * memory from a pool for an indeterminate amount of time, it may be appropriate + * to create an un-pooled `Buffer` instance using `Buffer.allocUnsafeSlow()` and + * then copying out the relevant bits. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Need to keep around a few small chunks of memory. + * const store = []; + * + * socket.on('readable', () => { + * let data; + * while (null !== (data = readable.read())) { + * // Allocate for retained data. + * const sb = Buffer.allocUnsafeSlow(10); + * + * // Copy the data into the new allocation. + * data.copy(sb, 0, 0, 10); + * + * store.push(sb); + * } + * }); + * ``` + * + * A `TypeError` will be thrown if `size` is not a number. + * @since v5.12.0 + * @param size The desired length of the new `Buffer`. + */ + allocUnsafeSlow(size: number): Buffer; + } + interface Buffer extends Uint8Array { + // see buffer.d.ts for implementation shared with all TypeScript versions + + /** + * Returns a new `Buffer` that references the same memory as the original, but + * offset and cropped by the `start` and `end` indices. + * + * This method is not compatible with the `Uint8Array.prototype.slice()`, + * which is a superclass of `Buffer`. To copy the slice, use`Uint8Array.prototype.slice()`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('buffer'); + * + * const copiedBuf = Uint8Array.prototype.slice.call(buf); + * copiedBuf[0]++; + * console.log(copiedBuf.toString()); + * // Prints: cuffer + * + * console.log(buf.toString()); + * // Prints: buffer + * + * // With buf.slice(), the original buffer is modified. + * const notReallyCopiedBuf = buf.slice(); + * notReallyCopiedBuf[0]++; + * console.log(notReallyCopiedBuf.toString()); + * // Prints: cuffer + * console.log(buf.toString()); + * // Also prints: cuffer (!) + * ``` + * @since v0.3.0 + * @deprecated Use `subarray` instead. + * @param [start=0] Where the new `Buffer` will start. + * @param [end=buf.length] Where the new `Buffer` will end (not inclusive). + */ + slice(start?: number, end?: number): Buffer; + /** + * Returns a new `Buffer` that references the same memory as the original, but + * offset and cropped by the `start` and `end` indices. + * + * Specifying `end` greater than `buf.length` will return the same result as + * that of `end` equal to `buf.length`. + * + * This method is inherited from [`TypedArray.prototype.subarray()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray). + * + * Modifying the new `Buffer` slice will modify the memory in the original `Buffer`because the allocated memory of the two objects overlap. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Create a `Buffer` with the ASCII alphabet, take a slice, and modify one byte + * // from the original `Buffer`. + * + * const buf1 = Buffer.allocUnsafe(26); + * + * for (let i = 0; i < 26; i++) { + * // 97 is the decimal ASCII value for 'a'. + * buf1[i] = i + 97; + * } + * + * const buf2 = buf1.subarray(0, 3); + * + * console.log(buf2.toString('ascii', 0, buf2.length)); + * // Prints: abc + * + * buf1[0] = 33; + * + * console.log(buf2.toString('ascii', 0, buf2.length)); + * // Prints: !bc + * ``` + * + * Specifying negative indexes causes the slice to be generated relative to the + * end of `buf` rather than the beginning. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('buffer'); + * + * console.log(buf.subarray(-6, -1).toString()); + * // Prints: buffe + * // (Equivalent to buf.subarray(0, 5).) + * + * console.log(buf.subarray(-6, -2).toString()); + * // Prints: buff + * // (Equivalent to buf.subarray(0, 4).) + * + * console.log(buf.subarray(-5, -2).toString()); + * // Prints: uff + * // (Equivalent to buf.subarray(1, 4).) + * ``` + * @since v3.0.0 + * @param [start=0] Where the new `Buffer` will start. + * @param [end=buf.length] Where the new `Buffer` will end (not inclusive). + */ + subarray(start?: number, end?: number): Buffer; + } + // TODO: remove globals in future version + /** + * @deprecated This is intended for internal use, and will be removed once `@types/node` no longer supports + * TypeScript versions earlier than 5.7. + */ + type NonSharedBuffer = Buffer; + /** + * @deprecated This is intended for internal use, and will be removed once `@types/node` no longer supports + * TypeScript versions earlier than 5.7. + */ + type AllowSharedBuffer = Buffer; + } + /** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */ + var SlowBuffer: { + /** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */ + new(size: number): Buffer; + prototype: Buffer; + }; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/buffer.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/buffer.d.ts new file mode 100644 index 00000000..354e08aa --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/buffer.d.ts @@ -0,0 +1,1934 @@ +// If lib.dom.d.ts or lib.webworker.d.ts is loaded, then use the global types. +// Otherwise, use the types from node. +type _Blob = typeof globalThis extends { onmessage: any; Blob: any } ? {} : import("buffer").Blob; +type _File = typeof globalThis extends { onmessage: any; File: any } ? {} : import("buffer").File; + +/** + * `Buffer` objects are used to represent a fixed-length sequence of bytes. Many + * Node.js APIs support `Buffer`s. + * + * The `Buffer` class is a subclass of JavaScript's [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) class and + * extends it with methods that cover additional use cases. Node.js APIs accept + * plain [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) s wherever `Buffer`s are supported as well. + * + * While the `Buffer` class is available within the global scope, it is still + * recommended to explicitly reference it via an import or require statement. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Creates a zero-filled Buffer of length 10. + * const buf1 = Buffer.alloc(10); + * + * // Creates a Buffer of length 10, + * // filled with bytes which all have the value `1`. + * const buf2 = Buffer.alloc(10, 1); + * + * // Creates an uninitialized buffer of length 10. + * // This is faster than calling Buffer.alloc() but the returned + * // Buffer instance might contain old data that needs to be + * // overwritten using fill(), write(), or other functions that fill the Buffer's + * // contents. + * const buf3 = Buffer.allocUnsafe(10); + * + * // Creates a Buffer containing the bytes [1, 2, 3]. + * const buf4 = Buffer.from([1, 2, 3]); + * + * // Creates a Buffer containing the bytes [1, 1, 1, 1] – the entries + * // are all truncated using `(value & 255)` to fit into the range 0–255. + * const buf5 = Buffer.from([257, 257.5, -255, '1']); + * + * // Creates a Buffer containing the UTF-8-encoded bytes for the string 'tést': + * // [0x74, 0xc3, 0xa9, 0x73, 0x74] (in hexadecimal notation) + * // [116, 195, 169, 115, 116] (in decimal notation) + * const buf6 = Buffer.from('tést'); + * + * // Creates a Buffer containing the Latin-1 bytes [0x74, 0xe9, 0x73, 0x74]. + * const buf7 = Buffer.from('tést', 'latin1'); + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/buffer.js) + */ +declare module "buffer" { + import { BinaryLike } from "node:crypto"; + import { ReadableStream as WebReadableStream } from "node:stream/web"; + /** + * This function returns `true` if `input` contains only valid UTF-8-encoded data, + * including the case in which `input` is empty. + * + * Throws if the `input` is a detached array buffer. + * @since v19.4.0, v18.14.0 + * @param input The input to validate. + */ + export function isUtf8(input: ArrayBuffer | NodeJS.TypedArray): boolean; + /** + * This function returns `true` if `input` contains only valid ASCII-encoded data, + * including the case in which `input` is empty. + * + * Throws if the `input` is a detached array buffer. + * @since v19.6.0, v18.15.0 + * @param input The input to validate. + */ + export function isAscii(input: ArrayBuffer | NodeJS.TypedArray): boolean; + export let INSPECT_MAX_BYTES: number; + export const kMaxLength: number; + export const kStringMaxLength: number; + export const constants: { + MAX_LENGTH: number; + MAX_STRING_LENGTH: number; + }; + export type TranscodeEncoding = + | "ascii" + | "utf8" + | "utf-8" + | "utf16le" + | "utf-16le" + | "ucs2" + | "ucs-2" + | "latin1" + | "binary"; + /** + * Re-encodes the given `Buffer` or `Uint8Array` instance from one character + * encoding to another. Returns a new `Buffer` instance. + * + * Throws if the `fromEnc` or `toEnc` specify invalid character encodings or if + * conversion from `fromEnc` to `toEnc` is not permitted. + * + * Encodings supported by `buffer.transcode()` are: `'ascii'`, `'utf8'`, `'utf16le'`, `'ucs2'`, `'latin1'`, and `'binary'`. + * + * The transcoding process will use substitution characters if a given byte + * sequence cannot be adequately represented in the target encoding. For instance: + * + * ```js + * import { Buffer, transcode } from 'node:buffer'; + * + * const newBuf = transcode(Buffer.from('€'), 'utf8', 'ascii'); + * console.log(newBuf.toString('ascii')); + * // Prints: '?' + * ``` + * + * Because the Euro (`€`) sign is not representable in US-ASCII, it is replaced + * with `?` in the transcoded `Buffer`. + * @since v7.1.0 + * @param source A `Buffer` or `Uint8Array` instance. + * @param fromEnc The current encoding. + * @param toEnc To target encoding. + */ + export function transcode( + source: Uint8Array, + fromEnc: TranscodeEncoding, + toEnc: TranscodeEncoding, + ): NonSharedBuffer; + /** + * Resolves a `'blob:nodedata:...'` an associated `Blob` object registered using + * a prior call to `URL.createObjectURL()`. + * @since v16.7.0 + * @param id A `'blob:nodedata:...` URL string returned by a prior call to `URL.createObjectURL()`. + */ + export function resolveObjectURL(id: string): Blob | undefined; + export { type AllowSharedBuffer, Buffer, type NonSharedBuffer }; + /** + * @experimental + */ + export interface BlobOptions { + /** + * One of either `'transparent'` or `'native'`. When set to `'native'`, line endings in string source parts + * will be converted to the platform native line-ending as specified by `import { EOL } from 'node:os'`. + */ + endings?: "transparent" | "native"; + /** + * The Blob content-type. The intent is for `type` to convey + * the MIME media type of the data, however no validation of the type format + * is performed. + */ + type?: string | undefined; + } + /** + * A `Blob` encapsulates immutable, raw data that can be safely shared across + * multiple worker threads. + * @since v15.7.0, v14.18.0 + */ + export class Blob { + /** + * The total size of the `Blob` in bytes. + * @since v15.7.0, v14.18.0 + */ + readonly size: number; + /** + * The content-type of the `Blob`. + * @since v15.7.0, v14.18.0 + */ + readonly type: string; + /** + * Creates a new `Blob` object containing a concatenation of the given sources. + * + * {ArrayBuffer}, {TypedArray}, {DataView}, and {Buffer} sources are copied into + * the 'Blob' and can therefore be safely modified after the 'Blob' is created. + * + * String sources are also copied into the `Blob`. + */ + constructor(sources: Array, options?: BlobOptions); + /** + * Returns a promise that fulfills with an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) containing a copy of + * the `Blob` data. + * @since v15.7.0, v14.18.0 + */ + arrayBuffer(): Promise; + /** + * The `blob.bytes()` method returns the byte of the `Blob` object as a `Promise`. + * + * ```js + * const blob = new Blob(['hello']); + * blob.bytes().then((bytes) => { + * console.log(bytes); // Outputs: Uint8Array(5) [ 104, 101, 108, 108, 111 ] + * }); + * ``` + */ + bytes(): Promise; + /** + * Creates and returns a new `Blob` containing a subset of this `Blob` objects + * data. The original `Blob` is not altered. + * @since v15.7.0, v14.18.0 + * @param start The starting index. + * @param end The ending index. + * @param type The content-type for the new `Blob` + */ + slice(start?: number, end?: number, type?: string): Blob; + /** + * Returns a promise that fulfills with the contents of the `Blob` decoded as a + * UTF-8 string. + * @since v15.7.0, v14.18.0 + */ + text(): Promise; + /** + * Returns a new `ReadableStream` that allows the content of the `Blob` to be read. + * @since v16.7.0 + */ + stream(): WebReadableStream; + } + export interface FileOptions { + /** + * One of either `'transparent'` or `'native'`. When set to `'native'`, line endings in string source parts will be + * converted to the platform native line-ending as specified by `import { EOL } from 'node:os'`. + */ + endings?: "native" | "transparent"; + /** The File content-type. */ + type?: string; + /** The last modified date of the file. `Default`: Date.now(). */ + lastModified?: number; + } + /** + * A [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) provides information about files. + * @since v19.2.0, v18.13.0 + */ + export class File extends Blob { + constructor(sources: Array, fileName: string, options?: FileOptions); + /** + * The name of the `File`. + * @since v19.2.0, v18.13.0 + */ + readonly name: string; + /** + * The last modified date of the `File`. + * @since v19.2.0, v18.13.0 + */ + readonly lastModified: number; + } + export import atob = globalThis.atob; + export import btoa = globalThis.btoa; + export type WithImplicitCoercion = + | T + | { valueOf(): T } + | (T extends string ? { [Symbol.toPrimitive](hint: "string"): T } : never); + global { + namespace NodeJS { + export { BufferEncoding }; + } + // Buffer class + type BufferEncoding = + | "ascii" + | "utf8" + | "utf-8" + | "utf16le" + | "utf-16le" + | "ucs2" + | "ucs-2" + | "base64" + | "base64url" + | "latin1" + | "binary" + | "hex"; + /** + * Raw data is stored in instances of the Buffer class. + * A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized. + * Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'base64url'|'binary'(deprecated)|'hex' + */ + interface BufferConstructor { + // see buffer.buffer.d.ts for implementation specific to TypeScript 5.7 and later + // see ts5.6/buffer.buffer.d.ts for implementation specific to TypeScript 5.6 and earlier + + /** + * Returns `true` if `obj` is a `Buffer`, `false` otherwise. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * Buffer.isBuffer(Buffer.alloc(10)); // true + * Buffer.isBuffer(Buffer.from('foo')); // true + * Buffer.isBuffer('a string'); // false + * Buffer.isBuffer([]); // false + * Buffer.isBuffer(new Uint8Array(1024)); // false + * ``` + * @since v0.1.101 + */ + isBuffer(obj: any): obj is Buffer; + /** + * Returns `true` if `encoding` is the name of a supported character encoding, + * or `false` otherwise. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * console.log(Buffer.isEncoding('utf8')); + * // Prints: true + * + * console.log(Buffer.isEncoding('hex')); + * // Prints: true + * + * console.log(Buffer.isEncoding('utf/8')); + * // Prints: false + * + * console.log(Buffer.isEncoding('')); + * // Prints: false + * ``` + * @since v0.9.1 + * @param encoding A character encoding name to check. + */ + isEncoding(encoding: string): encoding is BufferEncoding; + /** + * Returns the byte length of a string when encoded using `encoding`. + * This is not the same as [`String.prototype.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length), which does not account + * for the encoding that is used to convert the string into bytes. + * + * For `'base64'`, `'base64url'`, and `'hex'`, this function assumes valid input. + * For strings that contain non-base64/hex-encoded data (e.g. whitespace), the + * return value might be greater than the length of a `Buffer` created from the + * string. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const str = '\u00bd + \u00bc = \u00be'; + * + * console.log(`${str}: ${str.length} characters, ` + + * `${Buffer.byteLength(str, 'utf8')} bytes`); + * // Prints: ½ + ¼ = ¾: 9 characters, 12 bytes + * ``` + * + * When `string` is a + * `Buffer`/[`DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView)/[`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/- + * Reference/Global_Objects/TypedArray)/[`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)/[`SharedArrayBuffer`](https://develop- + * er.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer), the byte length as reported by `.byteLength`is returned. + * @since v0.1.90 + * @param string A value to calculate the length of. + * @param [encoding='utf8'] If `string` is a string, this is its encoding. + * @return The number of bytes contained within `string`. + */ + byteLength( + string: string | NodeJS.ArrayBufferView | ArrayBufferLike, + encoding?: BufferEncoding, + ): number; + /** + * Compares `buf1` to `buf2`, typically for the purpose of sorting arrays of `Buffer` instances. This is equivalent to calling `buf1.compare(buf2)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('1234'); + * const buf2 = Buffer.from('0123'); + * const arr = [buf1, buf2]; + * + * console.log(arr.sort(Buffer.compare)); + * // Prints: [ , ] + * // (This result is equal to: [buf2, buf1].) + * ``` + * @since v0.11.13 + * @return Either `-1`, `0`, or `1`, depending on the result of the comparison. See `compare` for details. + */ + compare(buf1: Uint8Array, buf2: Uint8Array): -1 | 0 | 1; + /** + * This is the size (in bytes) of pre-allocated internal `Buffer` instances used + * for pooling. This value may be modified. + * @since v0.11.3 + */ + poolSize: number; + } + interface Buffer { + // see buffer.buffer.d.ts for implementation specific to TypeScript 5.7 and later + // see ts5.6/buffer.buffer.d.ts for implementation specific to TypeScript 5.6 and earlier + + /** + * Writes `string` to `buf` at `offset` according to the character encoding in`encoding`. The `length` parameter is the number of bytes to write. If `buf` did + * not contain enough space to fit the entire string, only part of `string` will be + * written. However, partially encoded characters will not be written. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(256); + * + * const len = buf.write('\u00bd + \u00bc = \u00be', 0); + * + * console.log(`${len} bytes: ${buf.toString('utf8', 0, len)}`); + * // Prints: 12 bytes: ½ + ¼ = ¾ + * + * const buffer = Buffer.alloc(10); + * + * const length = buffer.write('abcd', 8); + * + * console.log(`${length} bytes: ${buffer.toString('utf8', 8, 10)}`); + * // Prints: 2 bytes : ab + * ``` + * @since v0.1.90 + * @param string String to write to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write `string`. + * @param [length=buf.length - offset] Maximum number of bytes to write (written bytes will not exceed `buf.length - offset`). + * @param [encoding='utf8'] The character encoding of `string`. + * @return Number of bytes written. + */ + write(string: string, encoding?: BufferEncoding): number; + write(string: string, offset: number, encoding?: BufferEncoding): number; + write(string: string, offset: number, length: number, encoding?: BufferEncoding): number; + /** + * Decodes `buf` to a string according to the specified character encoding in`encoding`. `start` and `end` may be passed to decode only a subset of `buf`. + * + * If `encoding` is `'utf8'` and a byte sequence in the input is not valid UTF-8, + * then each invalid byte is replaced with the replacement character `U+FFFD`. + * + * The maximum length of a string instance (in UTF-16 code units) is available + * as {@link constants.MAX_STRING_LENGTH}. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.allocUnsafe(26); + * + * for (let i = 0; i < 26; i++) { + * // 97 is the decimal ASCII value for 'a'. + * buf1[i] = i + 97; + * } + * + * console.log(buf1.toString('utf8')); + * // Prints: abcdefghijklmnopqrstuvwxyz + * console.log(buf1.toString('utf8', 0, 5)); + * // Prints: abcde + * + * const buf2 = Buffer.from('tést'); + * + * console.log(buf2.toString('hex')); + * // Prints: 74c3a97374 + * console.log(buf2.toString('utf8', 0, 3)); + * // Prints: té + * console.log(buf2.toString(undefined, 0, 3)); + * // Prints: té + * ``` + * @since v0.1.90 + * @param [encoding='utf8'] The character encoding to use. + * @param [start=0] The byte offset to start decoding at. + * @param [end=buf.length] The byte offset to stop decoding at (not inclusive). + */ + toString(encoding?: BufferEncoding, start?: number, end?: number): string; + /** + * Returns a JSON representation of `buf`. [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) implicitly calls + * this function when stringifying a `Buffer` instance. + * + * `Buffer.from()` accepts objects in the format returned from this method. + * In particular, `Buffer.from(buf.toJSON())` works like `Buffer.from(buf)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); + * const json = JSON.stringify(buf); + * + * console.log(json); + * // Prints: {"type":"Buffer","data":[1,2,3,4,5]} + * + * const copy = JSON.parse(json, (key, value) => { + * return value && value.type === 'Buffer' ? + * Buffer.from(value) : + * value; + * }); + * + * console.log(copy); + * // Prints: + * ``` + * @since v0.9.2 + */ + toJSON(): { + type: "Buffer"; + data: number[]; + }; + /** + * Returns `true` if both `buf` and `otherBuffer` have exactly the same bytes,`false` otherwise. Equivalent to `buf.compare(otherBuffer) === 0`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('ABC'); + * const buf2 = Buffer.from('414243', 'hex'); + * const buf3 = Buffer.from('ABCD'); + * + * console.log(buf1.equals(buf2)); + * // Prints: true + * console.log(buf1.equals(buf3)); + * // Prints: false + * ``` + * @since v0.11.13 + * @param otherBuffer A `Buffer` or {@link Uint8Array} with which to compare `buf`. + */ + equals(otherBuffer: Uint8Array): boolean; + /** + * Compares `buf` with `target` and returns a number indicating whether `buf`comes before, after, or is the same as `target` in sort order. + * Comparison is based on the actual sequence of bytes in each `Buffer`. + * + * * `0` is returned if `target` is the same as `buf` + * * `1` is returned if `target` should come _before_`buf` when sorted. + * * `-1` is returned if `target` should come _after_`buf` when sorted. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('ABC'); + * const buf2 = Buffer.from('BCD'); + * const buf3 = Buffer.from('ABCD'); + * + * console.log(buf1.compare(buf1)); + * // Prints: 0 + * console.log(buf1.compare(buf2)); + * // Prints: -1 + * console.log(buf1.compare(buf3)); + * // Prints: -1 + * console.log(buf2.compare(buf1)); + * // Prints: 1 + * console.log(buf2.compare(buf3)); + * // Prints: 1 + * console.log([buf1, buf2, buf3].sort(Buffer.compare)); + * // Prints: [ , , ] + * // (This result is equal to: [buf1, buf3, buf2].) + * ``` + * + * The optional `targetStart`, `targetEnd`, `sourceStart`, and `sourceEnd` arguments can be used to limit the comparison to specific ranges within `target` and `buf` respectively. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]); + * const buf2 = Buffer.from([5, 6, 7, 8, 9, 1, 2, 3, 4]); + * + * console.log(buf1.compare(buf2, 5, 9, 0, 4)); + * // Prints: 0 + * console.log(buf1.compare(buf2, 0, 6, 4)); + * // Prints: -1 + * console.log(buf1.compare(buf2, 5, 6, 5)); + * // Prints: 1 + * ``` + * + * `ERR_OUT_OF_RANGE` is thrown if `targetStart < 0`, `sourceStart < 0`, `targetEnd > target.byteLength`, or `sourceEnd > source.byteLength`. + * @since v0.11.13 + * @param target A `Buffer` or {@link Uint8Array} with which to compare `buf`. + * @param [targetStart=0] The offset within `target` at which to begin comparison. + * @param [targetEnd=target.length] The offset within `target` at which to end comparison (not inclusive). + * @param [sourceStart=0] The offset within `buf` at which to begin comparison. + * @param [sourceEnd=buf.length] The offset within `buf` at which to end comparison (not inclusive). + */ + compare( + target: Uint8Array, + targetStart?: number, + targetEnd?: number, + sourceStart?: number, + sourceEnd?: number, + ): -1 | 0 | 1; + /** + * Copies data from a region of `buf` to a region in `target`, even if the `target`memory region overlaps with `buf`. + * + * [`TypedArray.prototype.set()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set) performs the same operation, and is available + * for all TypedArrays, including Node.js `Buffer`s, although it takes + * different function arguments. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Create two `Buffer` instances. + * const buf1 = Buffer.allocUnsafe(26); + * const buf2 = Buffer.allocUnsafe(26).fill('!'); + * + * for (let i = 0; i < 26; i++) { + * // 97 is the decimal ASCII value for 'a'. + * buf1[i] = i + 97; + * } + * + * // Copy `buf1` bytes 16 through 19 into `buf2` starting at byte 8 of `buf2`. + * buf1.copy(buf2, 8, 16, 20); + * // This is equivalent to: + * // buf2.set(buf1.subarray(16, 20), 8); + * + * console.log(buf2.toString('ascii', 0, 25)); + * // Prints: !!!!!!!!qrst!!!!!!!!!!!!! + * ``` + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Create a `Buffer` and copy data from one region to an overlapping region + * // within the same `Buffer`. + * + * const buf = Buffer.allocUnsafe(26); + * + * for (let i = 0; i < 26; i++) { + * // 97 is the decimal ASCII value for 'a'. + * buf[i] = i + 97; + * } + * + * buf.copy(buf, 0, 4, 10); + * + * console.log(buf.toString()); + * // Prints: efghijghijklmnopqrstuvwxyz + * ``` + * @since v0.1.90 + * @param target A `Buffer` or {@link Uint8Array} to copy into. + * @param [targetStart=0] The offset within `target` at which to begin writing. + * @param [sourceStart=0] The offset within `buf` from which to begin copying. + * @param [sourceEnd=buf.length] The offset within `buf` at which to stop copying (not inclusive). + * @return The number of bytes copied. + */ + copy(target: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. + * + * `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigInt64BE(0x0102030405060708n, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigInt64BE(value: bigint, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. + * + * `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigInt64LE(0x0102030405060708n, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigInt64LE(value: bigint, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. + * + * This function is also available under the `writeBigUint64BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigUInt64BE(0xdecafafecacefaden, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigUInt64BE(value: bigint, offset?: number): number; + /** + * @alias Buffer.writeBigUInt64BE + * @since v14.10.0, v12.19.0 + */ + writeBigUint64BE(value: bigint, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigUInt64LE(0xdecafafecacefaden, 0); + * + * console.log(buf); + * // Prints: + * ``` + * + * This function is also available under the `writeBigUint64LE` alias. + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigUInt64LE(value: bigint, offset?: number): number; + /** + * @alias Buffer.writeBigUInt64LE + * @since v14.10.0, v12.19.0 + */ + writeBigUint64LE(value: bigint, offset?: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as little-endian. Supports up to 48 bits of accuracy. Behavior is undefined + * when `value` is anything other than an unsigned integer. + * + * This function is also available under the `writeUintLE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeUIntLE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeUIntLE(value: number, offset: number, byteLength: number): number; + /** + * @alias Buffer.writeUIntLE + * @since v14.9.0, v12.19.0 + */ + writeUintLE(value: number, offset: number, byteLength: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as big-endian. Supports up to 48 bits of accuracy. Behavior is undefined + * when `value` is anything other than an unsigned integer. + * + * This function is also available under the `writeUintBE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeUIntBE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeUIntBE(value: number, offset: number, byteLength: number): number; + /** + * @alias Buffer.writeUIntBE + * @since v14.9.0, v12.19.0 + */ + writeUintBE(value: number, offset: number, byteLength: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as little-endian. Supports up to 48 bits of accuracy. Behavior is undefined + * when `value` is anything other than a signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeIntLE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeIntLE(value: number, offset: number, byteLength: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as big-endian. Supports up to 48 bits of accuracy. Behavior is undefined when`value` is anything other than a + * signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeIntBE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeIntBE(value: number, offset: number, byteLength: number): number; + /** + * Reads an unsigned, big-endian 64-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readBigUint64BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + * + * console.log(buf.readBigUInt64BE(0)); + * // Prints: 4294967295n + * ``` + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigUInt64BE(offset?: number): bigint; + /** + * @alias Buffer.readBigUInt64BE + * @since v14.10.0, v12.19.0 + */ + readBigUint64BE(offset?: number): bigint; + /** + * Reads an unsigned, little-endian 64-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readBigUint64LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + * + * console.log(buf.readBigUInt64LE(0)); + * // Prints: 18446744069414584320n + * ``` + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigUInt64LE(offset?: number): bigint; + /** + * @alias Buffer.readBigUInt64LE + * @since v14.10.0, v12.19.0 + */ + readBigUint64LE(offset?: number): bigint; + /** + * Reads a signed, big-endian 64-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed + * values. + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigInt64BE(offset?: number): bigint; + /** + * Reads a signed, little-endian 64-bit integer from `buf` at the specified`offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed + * values. + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigInt64LE(offset?: number): bigint; + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as an unsigned, little-endian integer supporting + * up to 48 bits of accuracy. + * + * This function is also available under the `readUintLE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readUIntLE(0, 6).toString(16)); + * // Prints: ab9078563412 + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readUIntLE(offset: number, byteLength: number): number; + /** + * @alias Buffer.readUIntLE + * @since v14.9.0, v12.19.0 + */ + readUintLE(offset: number, byteLength: number): number; + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as an unsigned big-endian integer supporting + * up to 48 bits of accuracy. + * + * This function is also available under the `readUintBE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readUIntBE(0, 6).toString(16)); + * // Prints: 1234567890ab + * console.log(buf.readUIntBE(1, 6).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readUIntBE(offset: number, byteLength: number): number; + /** + * @alias Buffer.readUIntBE + * @since v14.9.0, v12.19.0 + */ + readUintBE(offset: number, byteLength: number): number; + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as a little-endian, two's complement signed value + * supporting up to 48 bits of accuracy. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readIntLE(0, 6).toString(16)); + * // Prints: -546f87a9cbee + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readIntLE(offset: number, byteLength: number): number; + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as a big-endian, two's complement signed value + * supporting up to 48 bits of accuracy. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readIntBE(0, 6).toString(16)); + * // Prints: 1234567890ab + * console.log(buf.readIntBE(1, 6).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * console.log(buf.readIntBE(1, 0).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readIntBE(offset: number, byteLength: number): number; + /** + * Reads an unsigned 8-bit integer from `buf` at the specified `offset`. + * + * This function is also available under the `readUint8` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, -2]); + * + * console.log(buf.readUInt8(0)); + * // Prints: 1 + * console.log(buf.readUInt8(1)); + * // Prints: 254 + * console.log(buf.readUInt8(2)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 1`. + */ + readUInt8(offset?: number): number; + /** + * @alias Buffer.readUInt8 + * @since v14.9.0, v12.19.0 + */ + readUint8(offset?: number): number; + /** + * Reads an unsigned, little-endian 16-bit integer from `buf` at the specified `offset`. + * + * This function is also available under the `readUint16LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56]); + * + * console.log(buf.readUInt16LE(0).toString(16)); + * // Prints: 3412 + * console.log(buf.readUInt16LE(1).toString(16)); + * // Prints: 5634 + * console.log(buf.readUInt16LE(2).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readUInt16LE(offset?: number): number; + /** + * @alias Buffer.readUInt16LE + * @since v14.9.0, v12.19.0 + */ + readUint16LE(offset?: number): number; + /** + * Reads an unsigned, big-endian 16-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readUint16BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56]); + * + * console.log(buf.readUInt16BE(0).toString(16)); + * // Prints: 1234 + * console.log(buf.readUInt16BE(1).toString(16)); + * // Prints: 3456 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readUInt16BE(offset?: number): number; + /** + * @alias Buffer.readUInt16BE + * @since v14.9.0, v12.19.0 + */ + readUint16BE(offset?: number): number; + /** + * Reads an unsigned, little-endian 32-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readUint32LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78]); + * + * console.log(buf.readUInt32LE(0).toString(16)); + * // Prints: 78563412 + * console.log(buf.readUInt32LE(1).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readUInt32LE(offset?: number): number; + /** + * @alias Buffer.readUInt32LE + * @since v14.9.0, v12.19.0 + */ + readUint32LE(offset?: number): number; + /** + * Reads an unsigned, big-endian 32-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readUint32BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78]); + * + * console.log(buf.readUInt32BE(0).toString(16)); + * // Prints: 12345678 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readUInt32BE(offset?: number): number; + /** + * @alias Buffer.readUInt32BE + * @since v14.9.0, v12.19.0 + */ + readUint32BE(offset?: number): number; + /** + * Reads a signed 8-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([-1, 5]); + * + * console.log(buf.readInt8(0)); + * // Prints: -1 + * console.log(buf.readInt8(1)); + * // Prints: 5 + * console.log(buf.readInt8(2)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 1`. + */ + readInt8(offset?: number): number; + /** + * Reads a signed, little-endian 16-bit integer from `buf` at the specified`offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 5]); + * + * console.log(buf.readInt16LE(0)); + * // Prints: 1280 + * console.log(buf.readInt16LE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readInt16LE(offset?: number): number; + /** + * Reads a signed, big-endian 16-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 5]); + * + * console.log(buf.readInt16BE(0)); + * // Prints: 5 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readInt16BE(offset?: number): number; + /** + * Reads a signed, little-endian 32-bit integer from `buf` at the specified`offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 0, 0, 5]); + * + * console.log(buf.readInt32LE(0)); + * // Prints: 83886080 + * console.log(buf.readInt32LE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readInt32LE(offset?: number): number; + /** + * Reads a signed, big-endian 32-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 0, 0, 5]); + * + * console.log(buf.readInt32BE(0)); + * // Prints: 5 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readInt32BE(offset?: number): number; + /** + * Reads a 32-bit, little-endian float from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4]); + * + * console.log(buf.readFloatLE(0)); + * // Prints: 1.539989614439558e-36 + * console.log(buf.readFloatLE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readFloatLE(offset?: number): number; + /** + * Reads a 32-bit, big-endian float from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4]); + * + * console.log(buf.readFloatBE(0)); + * // Prints: 2.387939260590663e-38 + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readFloatBE(offset?: number): number; + /** + * Reads a 64-bit, little-endian double from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + * + * console.log(buf.readDoubleLE(0)); + * // Prints: 5.447603722011605e-270 + * console.log(buf.readDoubleLE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 8`. + */ + readDoubleLE(offset?: number): number; + /** + * Reads a 64-bit, big-endian double from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + * + * console.log(buf.readDoubleBE(0)); + * // Prints: 8.20788039913184e-304 + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 8`. + */ + readDoubleBE(offset?: number): number; + reverse(): this; + /** + * Interprets `buf` as an array of unsigned 16-bit integers and swaps the + * byte order _in-place_. Throws `ERR_INVALID_BUFFER_SIZE` if `buf.length` is not a multiple of 2. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]); + * + * console.log(buf1); + * // Prints: + * + * buf1.swap16(); + * + * console.log(buf1); + * // Prints: + * + * const buf2 = Buffer.from([0x1, 0x2, 0x3]); + * + * buf2.swap16(); + * // Throws ERR_INVALID_BUFFER_SIZE. + * ``` + * + * One convenient use of `buf.swap16()` is to perform a fast in-place conversion + * between UTF-16 little-endian and UTF-16 big-endian: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('This is little-endian UTF-16', 'utf16le'); + * buf.swap16(); // Convert to big-endian UTF-16 text. + * ``` + * @since v5.10.0 + * @return A reference to `buf`. + */ + swap16(): this; + /** + * Interprets `buf` as an array of unsigned 32-bit integers and swaps the + * byte order _in-place_. Throws `ERR_INVALID_BUFFER_SIZE` if `buf.length` is not a multiple of 4. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]); + * + * console.log(buf1); + * // Prints: + * + * buf1.swap32(); + * + * console.log(buf1); + * // Prints: + * + * const buf2 = Buffer.from([0x1, 0x2, 0x3]); + * + * buf2.swap32(); + * // Throws ERR_INVALID_BUFFER_SIZE. + * ``` + * @since v5.10.0 + * @return A reference to `buf`. + */ + swap32(): this; + /** + * Interprets `buf` as an array of 64-bit numbers and swaps byte order _in-place_. + * Throws `ERR_INVALID_BUFFER_SIZE` if `buf.length` is not a multiple of 8. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]); + * + * console.log(buf1); + * // Prints: + * + * buf1.swap64(); + * + * console.log(buf1); + * // Prints: + * + * const buf2 = Buffer.from([0x1, 0x2, 0x3]); + * + * buf2.swap64(); + * // Throws ERR_INVALID_BUFFER_SIZE. + * ``` + * @since v6.3.0 + * @return A reference to `buf`. + */ + swap64(): this; + /** + * Writes `value` to `buf` at the specified `offset`. `value` must be a + * valid unsigned 8-bit integer. Behavior is undefined when `value` is anything + * other than an unsigned 8-bit integer. + * + * This function is also available under the `writeUint8` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt8(0x3, 0); + * buf.writeUInt8(0x4, 1); + * buf.writeUInt8(0x23, 2); + * buf.writeUInt8(0x42, 3); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 1`. + * @return `offset` plus the number of bytes written. + */ + writeUInt8(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt8 + * @since v14.9.0, v12.19.0 + */ + writeUint8(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid unsigned 16-bit integer. Behavior is undefined when `value` is + * anything other than an unsigned 16-bit integer. + * + * This function is also available under the `writeUint16LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt16LE(0xdead, 0); + * buf.writeUInt16LE(0xbeef, 2); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeUInt16LE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt16LE + * @since v14.9.0, v12.19.0 + */ + writeUint16LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid unsigned 16-bit integer. Behavior is undefined when `value`is anything other than an + * unsigned 16-bit integer. + * + * This function is also available under the `writeUint16BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt16BE(0xdead, 0); + * buf.writeUInt16BE(0xbeef, 2); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeUInt16BE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt16BE + * @since v14.9.0, v12.19.0 + */ + writeUint16BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid unsigned 32-bit integer. Behavior is undefined when `value` is + * anything other than an unsigned 32-bit integer. + * + * This function is also available under the `writeUint32LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt32LE(0xfeedface, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeUInt32LE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt32LE + * @since v14.9.0, v12.19.0 + */ + writeUint32LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid unsigned 32-bit integer. Behavior is undefined when `value`is anything other than an + * unsigned 32-bit integer. + * + * This function is also available under the `writeUint32BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt32BE(0xfeedface, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeUInt32BE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt32BE + * @since v14.9.0, v12.19.0 + */ + writeUint32BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset`. `value` must be a valid + * signed 8-bit integer. Behavior is undefined when `value` is anything other than + * a signed 8-bit integer. + * + * `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(2); + * + * buf.writeInt8(2, 0); + * buf.writeInt8(-2, 1); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 1`. + * @return `offset` plus the number of bytes written. + */ + writeInt8(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid signed 16-bit integer. Behavior is undefined when `value` is + * anything other than a signed 16-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(2); + * + * buf.writeInt16LE(0x0304, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeInt16LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid signed 16-bit integer. Behavior is undefined when `value` is + * anything other than a signed 16-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(2); + * + * buf.writeInt16BE(0x0102, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeInt16BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid signed 32-bit integer. Behavior is undefined when `value` is + * anything other than a signed 32-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeInt32LE(0x05060708, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeInt32LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid signed 32-bit integer. Behavior is undefined when `value` is + * anything other than a signed 32-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeInt32BE(0x01020304, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeInt32BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. Behavior is + * undefined when `value` is anything other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeFloatLE(0xcafebabe, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeFloatLE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. Behavior is + * undefined when `value` is anything other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeFloatBE(0xcafebabe, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeFloatBE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a JavaScript number. Behavior is undefined when `value` is anything + * other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeDoubleLE(123.456, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeDoubleLE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a JavaScript number. Behavior is undefined when `value` is anything + * other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeDoubleBE(123.456, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeDoubleBE(value: number, offset?: number): number; + /** + * Fills `buf` with the specified `value`. If the `offset` and `end` are not given, + * the entire `buf` will be filled: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Fill a `Buffer` with the ASCII character 'h'. + * + * const b = Buffer.allocUnsafe(50).fill('h'); + * + * console.log(b.toString()); + * // Prints: hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh + * + * // Fill a buffer with empty string + * const c = Buffer.allocUnsafe(5).fill(''); + * + * console.log(c.fill('')); + * // Prints: + * ``` + * + * `value` is coerced to a `uint32` value if it is not a string, `Buffer`, or + * integer. If the resulting integer is greater than `255` (decimal), `buf` will be + * filled with `value & 255`. + * + * If the final write of a `fill()` operation falls on a multi-byte character, + * then only the bytes of that character that fit into `buf` are written: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Fill a `Buffer` with character that takes up two bytes in UTF-8. + * + * console.log(Buffer.allocUnsafe(5).fill('\u0222')); + * // Prints: + * ``` + * + * If `value` contains invalid characters, it is truncated; if no valid + * fill data remains, an exception is thrown: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(5); + * + * console.log(buf.fill('a')); + * // Prints: + * console.log(buf.fill('aazz', 'hex')); + * // Prints: + * console.log(buf.fill('zz', 'hex')); + * // Throws an exception. + * ``` + * @since v0.5.0 + * @param value The value with which to fill `buf`. Empty value (string, Uint8Array, Buffer) is coerced to `0`. + * @param [offset=0] Number of bytes to skip before starting to fill `buf`. + * @param [end=buf.length] Where to stop filling `buf` (not inclusive). + * @param [encoding='utf8'] The encoding for `value` if `value` is a string. + * @return A reference to `buf`. + */ + fill(value: string | Uint8Array | number, offset?: number, end?: number, encoding?: BufferEncoding): this; + fill(value: string | Uint8Array | number, offset: number, encoding: BufferEncoding): this; + fill(value: string | Uint8Array | number, encoding: BufferEncoding): this; + /** + * If `value` is: + * + * * a string, `value` is interpreted according to the character encoding in `encoding`. + * * a `Buffer` or [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), `value` will be used in its entirety. + * To compare a partial `Buffer`, use `buf.subarray`. + * * a number, `value` will be interpreted as an unsigned 8-bit integer + * value between `0` and `255`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('this is a buffer'); + * + * console.log(buf.indexOf('this')); + * // Prints: 0 + * console.log(buf.indexOf('is')); + * // Prints: 2 + * console.log(buf.indexOf(Buffer.from('a buffer'))); + * // Prints: 8 + * console.log(buf.indexOf(97)); + * // Prints: 8 (97 is the decimal ASCII value for 'a') + * console.log(buf.indexOf(Buffer.from('a buffer example'))); + * // Prints: -1 + * console.log(buf.indexOf(Buffer.from('a buffer example').slice(0, 8))); + * // Prints: 8 + * + * const utf16Buffer = Buffer.from('\u039a\u0391\u03a3\u03a3\u0395', 'utf16le'); + * + * console.log(utf16Buffer.indexOf('\u03a3', 0, 'utf16le')); + * // Prints: 4 + * console.log(utf16Buffer.indexOf('\u03a3', -4, 'utf16le')); + * // Prints: 6 + * ``` + * + * If `value` is not a string, number, or `Buffer`, this method will throw a `TypeError`. If `value` is a number, it will be coerced to a valid byte value, + * an integer between 0 and 255. + * + * If `byteOffset` is not a number, it will be coerced to a number. If the result + * of coercion is `NaN` or `0`, then the entire buffer will be searched. This + * behavior matches [`String.prototype.indexOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf). + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const b = Buffer.from('abcdef'); + * + * // Passing a value that's a number, but not a valid byte. + * // Prints: 2, equivalent to searching for 99 or 'c'. + * console.log(b.indexOf(99.9)); + * console.log(b.indexOf(256 + 99)); + * + * // Passing a byteOffset that coerces to NaN or 0. + * // Prints: 1, searching the whole buffer. + * console.log(b.indexOf('b', undefined)); + * console.log(b.indexOf('b', {})); + * console.log(b.indexOf('b', null)); + * console.log(b.indexOf('b', [])); + * ``` + * + * If `value` is an empty string or empty `Buffer` and `byteOffset` is less + * than `buf.length`, `byteOffset` will be returned. If `value` is empty and`byteOffset` is at least `buf.length`, `buf.length` will be returned. + * @since v1.5.0 + * @param value What to search for. + * @param [byteOffset=0] Where to begin searching in `buf`. If negative, then offset is calculated from the end of `buf`. + * @param [encoding='utf8'] If `value` is a string, this is the encoding used to determine the binary representation of the string that will be searched for in `buf`. + * @return The index of the first occurrence of `value` in `buf`, or `-1` if `buf` does not contain `value`. + */ + indexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number; + indexOf(value: string | number | Uint8Array, encoding: BufferEncoding): number; + /** + * Identical to `buf.indexOf()`, except the last occurrence of `value` is found + * rather than the first occurrence. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('this buffer is a buffer'); + * + * console.log(buf.lastIndexOf('this')); + * // Prints: 0 + * console.log(buf.lastIndexOf('buffer')); + * // Prints: 17 + * console.log(buf.lastIndexOf(Buffer.from('buffer'))); + * // Prints: 17 + * console.log(buf.lastIndexOf(97)); + * // Prints: 15 (97 is the decimal ASCII value for 'a') + * console.log(buf.lastIndexOf(Buffer.from('yolo'))); + * // Prints: -1 + * console.log(buf.lastIndexOf('buffer', 5)); + * // Prints: 5 + * console.log(buf.lastIndexOf('buffer', 4)); + * // Prints: -1 + * + * const utf16Buffer = Buffer.from('\u039a\u0391\u03a3\u03a3\u0395', 'utf16le'); + * + * console.log(utf16Buffer.lastIndexOf('\u03a3', undefined, 'utf16le')); + * // Prints: 6 + * console.log(utf16Buffer.lastIndexOf('\u03a3', -5, 'utf16le')); + * // Prints: 4 + * ``` + * + * If `value` is not a string, number, or `Buffer`, this method will throw a `TypeError`. If `value` is a number, it will be coerced to a valid byte value, + * an integer between 0 and 255. + * + * If `byteOffset` is not a number, it will be coerced to a number. Any arguments + * that coerce to `NaN`, like `{}` or `undefined`, will search the whole buffer. + * This behavior matches [`String.prototype.lastIndexOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf). + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const b = Buffer.from('abcdef'); + * + * // Passing a value that's a number, but not a valid byte. + * // Prints: 2, equivalent to searching for 99 or 'c'. + * console.log(b.lastIndexOf(99.9)); + * console.log(b.lastIndexOf(256 + 99)); + * + * // Passing a byteOffset that coerces to NaN. + * // Prints: 1, searching the whole buffer. + * console.log(b.lastIndexOf('b', undefined)); + * console.log(b.lastIndexOf('b', {})); + * + * // Passing a byteOffset that coerces to 0. + * // Prints: -1, equivalent to passing 0. + * console.log(b.lastIndexOf('b', null)); + * console.log(b.lastIndexOf('b', [])); + * ``` + * + * If `value` is an empty string or empty `Buffer`, `byteOffset` will be returned. + * @since v6.0.0 + * @param value What to search for. + * @param [byteOffset=buf.length - 1] Where to begin searching in `buf`. If negative, then offset is calculated from the end of `buf`. + * @param [encoding='utf8'] If `value` is a string, this is the encoding used to determine the binary representation of the string that will be searched for in `buf`. + * @return The index of the last occurrence of `value` in `buf`, or `-1` if `buf` does not contain `value`. + */ + lastIndexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number; + lastIndexOf(value: string | number | Uint8Array, encoding: BufferEncoding): number; + /** + * Equivalent to `buf.indexOf() !== -1`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('this is a buffer'); + * + * console.log(buf.includes('this')); + * // Prints: true + * console.log(buf.includes('is')); + * // Prints: true + * console.log(buf.includes(Buffer.from('a buffer'))); + * // Prints: true + * console.log(buf.includes(97)); + * // Prints: true (97 is the decimal ASCII value for 'a') + * console.log(buf.includes(Buffer.from('a buffer example'))); + * // Prints: false + * console.log(buf.includes(Buffer.from('a buffer example').slice(0, 8))); + * // Prints: true + * console.log(buf.includes('this', 4)); + * // Prints: false + * ``` + * @since v5.3.0 + * @param value What to search for. + * @param [byteOffset=0] Where to begin searching in `buf`. If negative, then offset is calculated from the end of `buf`. + * @param [encoding='utf8'] If `value` is a string, this is its encoding. + * @return `true` if `value` was found in `buf`, `false` otherwise. + */ + includes(value: string | number | Buffer, byteOffset?: number, encoding?: BufferEncoding): boolean; + includes(value: string | number | Buffer, encoding: BufferEncoding): boolean; + } + var Buffer: BufferConstructor; + /** + * Decodes a string of Base64-encoded data into bytes, and encodes those bytes + * into a string using Latin-1 (ISO-8859-1). + * + * The `data` may be any JavaScript-value that can be coerced into a string. + * + * **This function is only provided for compatibility with legacy web platform APIs** + * **and should never be used in new code, because they use strings to represent** + * **binary data and predate the introduction of typed arrays in JavaScript.** + * **For code running using Node.js APIs, converting between base64-encoded strings** + * **and binary data should be performed using `Buffer.from(str, 'base64')` and `buf.toString('base64')`.** + * @since v15.13.0, v14.17.0 + * @legacy Use `Buffer.from(data, 'base64')` instead. + * @param data The Base64-encoded input string. + */ + function atob(data: string): string; + /** + * Decodes a string into bytes using Latin-1 (ISO-8859), and encodes those bytes + * into a string using Base64. + * + * The `data` may be any JavaScript-value that can be coerced into a string. + * + * **This function is only provided for compatibility with legacy web platform APIs** + * **and should never be used in new code, because they use strings to represent** + * **binary data and predate the introduction of typed arrays in JavaScript.** + * **For code running using Node.js APIs, converting between base64-encoded strings** + * **and binary data should be performed using `Buffer.from(str, 'base64')` and `buf.toString('base64')`.** + * @since v15.13.0, v14.17.0 + * @legacy Use `buf.toString('base64')` instead. + * @param data An ASCII (Latin1) string. + */ + function btoa(data: string): string; + interface Blob extends _Blob {} + /** + * `Blob` class is a global reference for `import { Blob } from 'node:buffer'` + * https://nodejs.org/api/buffer.html#class-blob + * @since v18.0.0 + */ + var Blob: typeof globalThis extends { onmessage: any; Blob: infer T } ? T + : typeof import("buffer").Blob; + interface File extends _File {} + /** + * `File` class is a global reference for `import { File } from 'node:buffer'` + * https://nodejs.org/api/buffer.html#class-file + * @since v20.0.0 + */ + var File: typeof globalThis extends { onmessage: any; File: infer T } ? T + : typeof import("buffer").File; + } +} +declare module "node:buffer" { + export * from "buffer"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/child_process.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/child_process.d.ts new file mode 100644 index 00000000..313c33c4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/child_process.d.ts @@ -0,0 +1,1476 @@ +/** + * The `node:child_process` module provides the ability to spawn subprocesses in + * a manner that is similar, but not identical, to [`popen(3)`](http://man7.org/linux/man-pages/man3/popen.3.html). This capability + * is primarily provided by the {@link spawn} function: + * + * ```js + * import { spawn } from 'node:child_process'; + * const ls = spawn('ls', ['-lh', '/usr']); + * + * ls.stdout.on('data', (data) => { + * console.log(`stdout: ${data}`); + * }); + * + * ls.stderr.on('data', (data) => { + * console.error(`stderr: ${data}`); + * }); + * + * ls.on('close', (code) => { + * console.log(`child process exited with code ${code}`); + * }); + * ``` + * + * By default, pipes for `stdin`, `stdout`, and `stderr` are established between + * the parent Node.js process and the spawned subprocess. These pipes have + * limited (and platform-specific) capacity. If the subprocess writes to + * stdout in excess of that limit without the output being captured, the + * subprocess blocks, waiting for the pipe buffer to accept more data. This is + * identical to the behavior of pipes in the shell. Use the `{ stdio: 'ignore' }` option if the output will not be consumed. + * + * The command lookup is performed using the `options.env.PATH` environment + * variable if `env` is in the `options` object. Otherwise, `process.env.PATH` is + * used. If `options.env` is set without `PATH`, lookup on Unix is performed + * on a default search path search of `/usr/bin:/bin` (see your operating system's + * manual for execvpe/execvp), on Windows the current processes environment + * variable `PATH` is used. + * + * On Windows, environment variables are case-insensitive. Node.js + * lexicographically sorts the `env` keys and uses the first one that + * case-insensitively matches. Only first (in lexicographic order) entry will be + * passed to the subprocess. This might lead to issues on Windows when passing + * objects to the `env` option that have multiple variants of the same key, such as `PATH` and `Path`. + * + * The {@link spawn} method spawns the child process asynchronously, + * without blocking the Node.js event loop. The {@link spawnSync} function provides equivalent functionality in a synchronous manner that blocks + * the event loop until the spawned process either exits or is terminated. + * + * For convenience, the `node:child_process` module provides a handful of + * synchronous and asynchronous alternatives to {@link spawn} and {@link spawnSync}. Each of these alternatives are implemented on + * top of {@link spawn} or {@link spawnSync}. + * + * * {@link exec}: spawns a shell and runs a command within that + * shell, passing the `stdout` and `stderr` to a callback function when + * complete. + * * {@link execFile}: similar to {@link exec} except + * that it spawns the command directly without first spawning a shell by + * default. + * * {@link fork}: spawns a new Node.js process and invokes a + * specified module with an IPC communication channel established that allows + * sending messages between parent and child. + * * {@link execSync}: a synchronous version of {@link exec} that will block the Node.js event loop. + * * {@link execFileSync}: a synchronous version of {@link execFile} that will block the Node.js event loop. + * + * For certain use cases, such as automating shell scripts, the `synchronous counterparts` may be more convenient. In many cases, however, + * the synchronous methods can have significant impact on performance due to + * stalling the event loop while spawned processes complete. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/child_process.js) + */ +declare module "child_process" { + import { NonSharedBuffer } from "node:buffer"; + import { Abortable, EventEmitter } from "node:events"; + import * as dgram from "node:dgram"; + import * as net from "node:net"; + import { Readable, Stream, Writable } from "node:stream"; + import { URL } from "node:url"; + type Serializable = string | object | number | boolean | bigint; + type SendHandle = net.Socket | net.Server | dgram.Socket | undefined; + /** + * Instances of the `ChildProcess` represent spawned child processes. + * + * Instances of `ChildProcess` are not intended to be created directly. Rather, + * use the {@link spawn}, {@link exec},{@link execFile}, or {@link fork} methods to create + * instances of `ChildProcess`. + * @since v2.2.0 + */ + class ChildProcess extends EventEmitter { + /** + * A `Writable Stream` that represents the child process's `stdin`. + * + * If a child process waits to read all of its input, the child will not continue + * until this stream has been closed via `end()`. + * + * If the child was spawned with `stdio[0]` set to anything other than `'pipe'`, + * then this will be `null`. + * + * `subprocess.stdin` is an alias for `subprocess.stdio[0]`. Both properties will + * refer to the same value. + * + * The `subprocess.stdin` property can be `null` or `undefined` if the child process could not be successfully spawned. + * @since v0.1.90 + */ + stdin: Writable | null; + /** + * A `Readable Stream` that represents the child process's `stdout`. + * + * If the child was spawned with `stdio[1]` set to anything other than `'pipe'`, + * then this will be `null`. + * + * `subprocess.stdout` is an alias for `subprocess.stdio[1]`. Both properties will + * refer to the same value. + * + * ```js + * import { spawn } from 'node:child_process'; + * + * const subprocess = spawn('ls'); + * + * subprocess.stdout.on('data', (data) => { + * console.log(`Received chunk ${data}`); + * }); + * ``` + * + * The `subprocess.stdout` property can be `null` or `undefined` if the child process could not be successfully spawned. + * @since v0.1.90 + */ + stdout: Readable | null; + /** + * A `Readable Stream` that represents the child process's `stderr`. + * + * If the child was spawned with `stdio[2]` set to anything other than `'pipe'`, + * then this will be `null`. + * + * `subprocess.stderr` is an alias for `subprocess.stdio[2]`. Both properties will + * refer to the same value. + * + * The `subprocess.stderr` property can be `null` or `undefined` if the child process could not be successfully spawned. + * @since v0.1.90 + */ + stderr: Readable | null; + /** + * The `subprocess.channel` property is a reference to the child's IPC channel. If + * no IPC channel exists, this property is `undefined`. + * @since v7.1.0 + */ + readonly channel?: Control | null; + /** + * A sparse array of pipes to the child process, corresponding with positions in + * the `stdio` option passed to {@link spawn} that have been set + * to the value `'pipe'`. `subprocess.stdio[0]`, `subprocess.stdio[1]`, and `subprocess.stdio[2]` are also available as `subprocess.stdin`, `subprocess.stdout`, and `subprocess.stderr`, + * respectively. + * + * In the following example, only the child's fd `1` (stdout) is configured as a + * pipe, so only the parent's `subprocess.stdio[1]` is a stream, all other values + * in the array are `null`. + * + * ```js + * import assert from 'node:assert'; + * import fs from 'node:fs'; + * import child_process from 'node:child_process'; + * + * const subprocess = child_process.spawn('ls', { + * stdio: [ + * 0, // Use parent's stdin for child. + * 'pipe', // Pipe child's stdout to parent. + * fs.openSync('err.out', 'w'), // Direct child's stderr to a file. + * ], + * }); + * + * assert.strictEqual(subprocess.stdio[0], null); + * assert.strictEqual(subprocess.stdio[0], subprocess.stdin); + * + * assert(subprocess.stdout); + * assert.strictEqual(subprocess.stdio[1], subprocess.stdout); + * + * assert.strictEqual(subprocess.stdio[2], null); + * assert.strictEqual(subprocess.stdio[2], subprocess.stderr); + * ``` + * + * The `subprocess.stdio` property can be `undefined` if the child process could + * not be successfully spawned. + * @since v0.7.10 + */ + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + /** + * The `subprocess.killed` property indicates whether the child process + * successfully received a signal from `subprocess.kill()`. The `killed` property + * does not indicate that the child process has been terminated. + * @since v0.5.10 + */ + readonly killed: boolean; + /** + * Returns the process identifier (PID) of the child process. If the child process + * fails to spawn due to errors, then the value is `undefined` and `error` is + * emitted. + * + * ```js + * import { spawn } from 'node:child_process'; + * const grep = spawn('grep', ['ssh']); + * + * console.log(`Spawned child pid: ${grep.pid}`); + * grep.stdin.end(); + * ``` + * @since v0.1.90 + */ + readonly pid?: number | undefined; + /** + * The `subprocess.connected` property indicates whether it is still possible to + * send and receive messages from a child process. When `subprocess.connected` is `false`, it is no longer possible to send or receive messages. + * @since v0.7.2 + */ + readonly connected: boolean; + /** + * The `subprocess.exitCode` property indicates the exit code of the child process. + * If the child process is still running, the field will be `null`. + */ + readonly exitCode: number | null; + /** + * The `subprocess.signalCode` property indicates the signal received by + * the child process if any, else `null`. + */ + readonly signalCode: NodeJS.Signals | null; + /** + * The `subprocess.spawnargs` property represents the full list of command-line + * arguments the child process was launched with. + */ + readonly spawnargs: string[]; + /** + * The `subprocess.spawnfile` property indicates the executable file name of + * the child process that is launched. + * + * For {@link fork}, its value will be equal to `process.execPath`. + * For {@link spawn}, its value will be the name of + * the executable file. + * For {@link exec}, its value will be the name of the shell + * in which the child process is launched. + */ + readonly spawnfile: string; + /** + * The `subprocess.kill()` method sends a signal to the child process. If no + * argument is given, the process will be sent the `'SIGTERM'` signal. See [`signal(7)`](http://man7.org/linux/man-pages/man7/signal.7.html) for a list of available signals. This function + * returns `true` if [`kill(2)`](http://man7.org/linux/man-pages/man2/kill.2.html) succeeds, and `false` otherwise. + * + * ```js + * import { spawn } from 'node:child_process'; + * const grep = spawn('grep', ['ssh']); + * + * grep.on('close', (code, signal) => { + * console.log( + * `child process terminated due to receipt of signal ${signal}`); + * }); + * + * // Send SIGHUP to process. + * grep.kill('SIGHUP'); + * ``` + * + * The `ChildProcess` object may emit an `'error'` event if the signal + * cannot be delivered. Sending a signal to a child process that has already exited + * is not an error but may have unforeseen consequences. Specifically, if the + * process identifier (PID) has been reassigned to another process, the signal will + * be delivered to that process instead which can have unexpected results. + * + * While the function is called `kill`, the signal delivered to the child process + * may not actually terminate the process. + * + * See [`kill(2)`](http://man7.org/linux/man-pages/man2/kill.2.html) for reference. + * + * On Windows, where POSIX signals do not exist, the `signal` argument will be + * ignored, and the process will be killed forcefully and abruptly (similar to `'SIGKILL'`). + * See `Signal Events` for more details. + * + * On Linux, child processes of child processes will not be terminated + * when attempting to kill their parent. This is likely to happen when running a + * new process in a shell or with the use of the `shell` option of `ChildProcess`: + * + * ```js + * 'use strict'; + * import { spawn } from 'node:child_process'; + * + * const subprocess = spawn( + * 'sh', + * [ + * '-c', + * `node -e "setInterval(() => { + * console.log(process.pid, 'is alive') + * }, 500);"`, + * ], { + * stdio: ['inherit', 'inherit', 'inherit'], + * }, + * ); + * + * setTimeout(() => { + * subprocess.kill(); // Does not terminate the Node.js process in the shell. + * }, 2000); + * ``` + * @since v0.1.90 + */ + kill(signal?: NodeJS.Signals | number): boolean; + /** + * Calls {@link ChildProcess.kill} with `'SIGTERM'`. + * @since v20.5.0 + */ + [Symbol.dispose](): void; + /** + * When an IPC channel has been established between the parent and child ( + * i.e. when using {@link fork}), the `subprocess.send()` method can + * be used to send messages to the child process. When the child process is a + * Node.js instance, these messages can be received via the `'message'` event. + * + * The message goes through serialization and parsing. The resulting + * message might not be the same as what is originally sent. + * + * For example, in the parent script: + * + * ```js + * import cp from 'node:child_process'; + * const n = cp.fork(`${__dirname}/sub.js`); + * + * n.on('message', (m) => { + * console.log('PARENT got message:', m); + * }); + * + * // Causes the child to print: CHILD got message: { hello: 'world' } + * n.send({ hello: 'world' }); + * ``` + * + * And then the child script, `'sub.js'` might look like this: + * + * ```js + * process.on('message', (m) => { + * console.log('CHILD got message:', m); + * }); + * + * // Causes the parent to print: PARENT got message: { foo: 'bar', baz: null } + * process.send({ foo: 'bar', baz: NaN }); + * ``` + * + * Child Node.js processes will have a `process.send()` method of their own + * that allows the child to send messages back to the parent. + * + * There is a special case when sending a `{cmd: 'NODE_foo'}` message. Messages + * containing a `NODE_` prefix in the `cmd` property are reserved for use within + * Node.js core and will not be emitted in the child's `'message'` event. Rather, such messages are emitted using the `'internalMessage'` event and are consumed internally by Node.js. + * Applications should avoid using such messages or listening for `'internalMessage'` events as it is subject to change without notice. + * + * The optional `sendHandle` argument that may be passed to `subprocess.send()` is + * for passing a TCP server or socket object to the child process. The child will + * receive the object as the second argument passed to the callback function + * registered on the `'message'` event. Any data that is received and buffered in + * the socket will not be sent to the child. Sending IPC sockets is not supported on Windows. + * + * The optional `callback` is a function that is invoked after the message is + * sent but before the child may have received it. The function is called with a + * single argument: `null` on success, or an `Error` object on failure. + * + * If no `callback` function is provided and the message cannot be sent, an `'error'` event will be emitted by the `ChildProcess` object. This can + * happen, for instance, when the child process has already exited. + * + * `subprocess.send()` will return `false` if the channel has closed or when the + * backlog of unsent messages exceeds a threshold that makes it unwise to send + * more. Otherwise, the method returns `true`. The `callback` function can be + * used to implement flow control. + * + * #### Example: sending a server object + * + * The `sendHandle` argument can be used, for instance, to pass the handle of + * a TCP server object to the child process as illustrated in the example below: + * + * ```js + * import { createServer } from 'node:net'; + * import { fork } from 'node:child_process'; + * const subprocess = fork('subprocess.js'); + * + * // Open up the server object and send the handle. + * const server = createServer(); + * server.on('connection', (socket) => { + * socket.end('handled by parent'); + * }); + * server.listen(1337, () => { + * subprocess.send('server', server); + * }); + * ``` + * + * The child would then receive the server object as: + * + * ```js + * process.on('message', (m, server) => { + * if (m === 'server') { + * server.on('connection', (socket) => { + * socket.end('handled by child'); + * }); + * } + * }); + * ``` + * + * Once the server is now shared between the parent and child, some connections + * can be handled by the parent and some by the child. + * + * While the example above uses a server created using the `node:net` module, `node:dgram` module servers use exactly the same workflow with the exceptions of + * listening on a `'message'` event instead of `'connection'` and using `server.bind()` instead of `server.listen()`. This is, however, only + * supported on Unix platforms. + * + * #### Example: sending a socket object + * + * Similarly, the `sendHandler` argument can be used to pass the handle of a + * socket to the child process. The example below spawns two children that each + * handle connections with "normal" or "special" priority: + * + * ```js + * import { createServer } from 'node:net'; + * import { fork } from 'node:child_process'; + * const normal = fork('subprocess.js', ['normal']); + * const special = fork('subprocess.js', ['special']); + * + * // Open up the server and send sockets to child. Use pauseOnConnect to prevent + * // the sockets from being read before they are sent to the child process. + * const server = createServer({ pauseOnConnect: true }); + * server.on('connection', (socket) => { + * + * // If this is special priority... + * if (socket.remoteAddress === '74.125.127.100') { + * special.send('socket', socket); + * return; + * } + * // This is normal priority. + * normal.send('socket', socket); + * }); + * server.listen(1337); + * ``` + * + * The `subprocess.js` would receive the socket handle as the second argument + * passed to the event callback function: + * + * ```js + * process.on('message', (m, socket) => { + * if (m === 'socket') { + * if (socket) { + * // Check that the client socket exists. + * // It is possible for the socket to be closed between the time it is + * // sent and the time it is received in the child process. + * socket.end(`Request handled with ${process.argv[2]} priority`); + * } + * } + * }); + * ``` + * + * Do not use `.maxConnections` on a socket that has been passed to a subprocess. + * The parent cannot track when the socket is destroyed. + * + * Any `'message'` handlers in the subprocess should verify that `socket` exists, + * as the connection may have been closed during the time it takes to send the + * connection to the child. + * @since v0.5.9 + * @param sendHandle `undefined`, or a [`net.Socket`](https://nodejs.org/docs/latest-v22.x/api/net.html#class-netsocket), [`net.Server`](https://nodejs.org/docs/latest-v22.x/api/net.html#class-netserver), or [`dgram.Socket`](https://nodejs.org/docs/latest-v22.x/api/dgram.html#class-dgramsocket) object. + * @param options The `options` argument, if present, is an object used to parameterize the sending of certain types of handles. `options` supports the following properties: + */ + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + /** + * Closes the IPC channel between parent and child, allowing the child to exit + * gracefully once there are no other connections keeping it alive. After calling + * this method the `subprocess.connected` and `process.connected` properties in + * both the parent and child (respectively) will be set to `false`, and it will be + * no longer possible to pass messages between the processes. + * + * The `'disconnect'` event will be emitted when there are no messages in the + * process of being received. This will most often be triggered immediately after + * calling `subprocess.disconnect()`. + * + * When the child process is a Node.js instance (e.g. spawned using {@link fork}), the `process.disconnect()` method can be invoked + * within the child process to close the IPC channel as well. + * @since v0.7.2 + */ + disconnect(): void; + /** + * By default, the parent will wait for the detached child to exit. To prevent the + * parent from waiting for a given `subprocess` to exit, use the `subprocess.unref()` method. Doing so will cause the parent's event loop to not + * include the child in its reference count, allowing the parent to exit + * independently of the child, unless there is an established IPC channel between + * the child and the parent. + * + * ```js + * import { spawn } from 'node:child_process'; + * + * const subprocess = spawn(process.argv[0], ['child_program.js'], { + * detached: true, + * stdio: 'ignore', + * }); + * + * subprocess.unref(); + * ``` + * @since v0.7.10 + */ + unref(): void; + /** + * Calling `subprocess.ref()` after making a call to `subprocess.unref()` will + * restore the removed reference count for the child process, forcing the parent + * to wait for the child to exit before exiting itself. + * + * ```js + * import { spawn } from 'node:child_process'; + * + * const subprocess = spawn(process.argv[0], ['child_program.js'], { + * detached: true, + * stdio: 'ignore', + * }); + * + * subprocess.unref(); + * subprocess.ref(); + * ``` + * @since v0.7.10 + */ + ref(): void; + /** + * events.EventEmitter + * 1. close + * 2. disconnect + * 3. error + * 4. exit + * 5. message + * 6. spawn + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + addListener(event: "disconnect", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + addListener(event: "message", listener: (message: Serializable, sendHandle: SendHandle) => void): this; + addListener(event: "spawn", listener: () => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "close", code: number | null, signal: NodeJS.Signals | null): boolean; + emit(event: "disconnect"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "exit", code: number | null, signal: NodeJS.Signals | null): boolean; + emit(event: "message", message: Serializable, sendHandle: SendHandle): boolean; + emit(event: "spawn", listener: () => void): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + on(event: "disconnect", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + on(event: "message", listener: (message: Serializable, sendHandle: SendHandle) => void): this; + on(event: "spawn", listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + once(event: "disconnect", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + once(event: "message", listener: (message: Serializable, sendHandle: SendHandle) => void): this; + once(event: "spawn", listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + prependListener(event: "disconnect", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + prependListener(event: "message", listener: (message: Serializable, sendHandle: SendHandle) => void): this; + prependListener(event: "spawn", listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener( + event: "close", + listener: (code: number | null, signal: NodeJS.Signals | null) => void, + ): this; + prependOnceListener(event: "disconnect", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener( + event: "exit", + listener: (code: number | null, signal: NodeJS.Signals | null) => void, + ): this; + prependOnceListener(event: "message", listener: (message: Serializable, sendHandle: SendHandle) => void): this; + prependOnceListener(event: "spawn", listener: () => void): this; + } + // return this object when stdio option is undefined or not specified + interface ChildProcessWithoutNullStreams extends ChildProcess { + stdin: Writable; + stdout: Readable; + stderr: Readable; + readonly stdio: [ + Writable, + Readable, + Readable, + // stderr + Readable | Writable | null | undefined, + // extra, no modification + Readable | Writable | null | undefined, // extra, no modification + ]; + } + // return this object when stdio option is a tuple of 3 + interface ChildProcessByStdio + extends ChildProcess + { + stdin: I; + stdout: O; + stderr: E; + readonly stdio: [ + I, + O, + E, + Readable | Writable | null | undefined, + // extra, no modification + Readable | Writable | null | undefined, // extra, no modification + ]; + } + interface Control extends EventEmitter { + ref(): void; + unref(): void; + } + interface MessageOptions { + keepOpen?: boolean | undefined; + } + type IOType = "overlapped" | "pipe" | "ignore" | "inherit"; + type StdioOptions = IOType | Array; + type SerializationType = "json" | "advanced"; + interface MessagingOptions extends Abortable { + /** + * Specify the kind of serialization used for sending messages between processes. + * @default 'json' + */ + serialization?: SerializationType | undefined; + /** + * The signal value to be used when the spawned process will be killed by the abort signal. + * @default 'SIGTERM' + */ + killSignal?: NodeJS.Signals | number | undefined; + /** + * In milliseconds the maximum amount of time the process is allowed to run. + */ + timeout?: number | undefined; + } + interface ProcessEnvOptions { + uid?: number | undefined; + gid?: number | undefined; + cwd?: string | URL | undefined; + env?: NodeJS.ProcessEnv | undefined; + } + interface CommonOptions extends ProcessEnvOptions { + /** + * @default false + */ + windowsHide?: boolean | undefined; + /** + * @default 0 + */ + timeout?: number | undefined; + } + interface CommonSpawnOptions extends CommonOptions, MessagingOptions, Abortable { + argv0?: string | undefined; + /** + * Can be set to 'pipe', 'inherit', 'overlapped', or 'ignore', or an array of these strings. + * If passed as an array, the first element is used for `stdin`, the second for + * `stdout`, and the third for `stderr`. A fourth element can be used to + * specify the `stdio` behavior beyond the standard streams. See + * {@link ChildProcess.stdio} for more information. + * + * @default 'pipe' + */ + stdio?: StdioOptions | undefined; + shell?: boolean | string | undefined; + windowsVerbatimArguments?: boolean | undefined; + } + interface SpawnOptions extends CommonSpawnOptions { + detached?: boolean | undefined; + } + interface SpawnOptionsWithoutStdio extends SpawnOptions { + stdio?: StdioPipeNamed | StdioPipe[] | undefined; + } + type StdioNull = "inherit" | "ignore" | Stream; + type StdioPipeNamed = "pipe" | "overlapped"; + type StdioPipe = undefined | null | StdioPipeNamed; + interface SpawnOptionsWithStdioTuple< + Stdin extends StdioNull | StdioPipe, + Stdout extends StdioNull | StdioPipe, + Stderr extends StdioNull | StdioPipe, + > extends SpawnOptions { + stdio: [Stdin, Stdout, Stderr]; + } + /** + * The `child_process.spawn()` method spawns a new process using the given `command`, with command-line arguments in `args`. If omitted, `args` defaults + * to an empty array. + * + * **If the `shell` option is enabled, do not pass unsanitized user input to this** + * **function. Any input containing shell metacharacters may be used to trigger** + * **arbitrary command execution.** + * + * A third argument may be used to specify additional options, with these defaults: + * + * ```js + * const defaults = { + * cwd: undefined, + * env: process.env, + * }; + * ``` + * + * Use `cwd` to specify the working directory from which the process is spawned. + * If not given, the default is to inherit the current working directory. If given, + * but the path does not exist, the child process emits an `ENOENT` error + * and exits immediately. `ENOENT` is also emitted when the command + * does not exist. + * + * Use `env` to specify environment variables that will be visible to the new + * process, the default is `process.env`. + * + * `undefined` values in `env` will be ignored. + * + * Example of running `ls -lh /usr`, capturing `stdout`, `stderr`, and the + * exit code: + * + * ```js + * import { spawn } from 'node:child_process'; + * const ls = spawn('ls', ['-lh', '/usr']); + * + * ls.stdout.on('data', (data) => { + * console.log(`stdout: ${data}`); + * }); + * + * ls.stderr.on('data', (data) => { + * console.error(`stderr: ${data}`); + * }); + * + * ls.on('close', (code) => { + * console.log(`child process exited with code ${code}`); + * }); + * ``` + * + * Example: A very elaborate way to run `ps ax | grep ssh` + * + * ```js + * import { spawn } from 'node:child_process'; + * const ps = spawn('ps', ['ax']); + * const grep = spawn('grep', ['ssh']); + * + * ps.stdout.on('data', (data) => { + * grep.stdin.write(data); + * }); + * + * ps.stderr.on('data', (data) => { + * console.error(`ps stderr: ${data}`); + * }); + * + * ps.on('close', (code) => { + * if (code !== 0) { + * console.log(`ps process exited with code ${code}`); + * } + * grep.stdin.end(); + * }); + * + * grep.stdout.on('data', (data) => { + * console.log(data.toString()); + * }); + * + * grep.stderr.on('data', (data) => { + * console.error(`grep stderr: ${data}`); + * }); + * + * grep.on('close', (code) => { + * if (code !== 0) { + * console.log(`grep process exited with code ${code}`); + * } + * }); + * ``` + * + * Example of checking for failed `spawn`: + * + * ```js + * import { spawn } from 'node:child_process'; + * const subprocess = spawn('bad_command'); + * + * subprocess.on('error', (err) => { + * console.error('Failed to start subprocess.'); + * }); + * ``` + * + * Certain platforms (macOS, Linux) will use the value of `argv[0]` for the process + * title while others (Windows, SunOS) will use `command`. + * + * Node.js overwrites `argv[0]` with `process.execPath` on startup, so `process.argv[0]` in a Node.js child process will not match the `argv0` parameter passed to `spawn` from the parent. Retrieve + * it with the `process.argv0` property instead. + * + * If the `signal` option is enabled, calling `.abort()` on the corresponding `AbortController` is similar to calling `.kill()` on the child process except + * the error passed to the callback will be an `AbortError`: + * + * ```js + * import { spawn } from 'node:child_process'; + * const controller = new AbortController(); + * const { signal } = controller; + * const grep = spawn('grep', ['ssh'], { signal }); + * grep.on('error', (err) => { + * // This will be called with err being an AbortError if the controller aborts + * }); + * controller.abort(); // Stops the child process + * ``` + * @since v0.1.90 + * @param command The command to run. + * @param args List of string arguments. + */ + function spawn(command: string, options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn(command: string, options: SpawnOptions): ChildProcess; + // overloads of spawn with 'args' + function spawn( + command: string, + args?: readonly string[], + options?: SpawnOptionsWithoutStdio, + ): ChildProcessWithoutNullStreams; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn( + command: string, + args: readonly string[], + options: SpawnOptionsWithStdioTuple, + ): ChildProcessByStdio; + function spawn(command: string, args: readonly string[], options: SpawnOptions): ChildProcess; + interface ExecOptions extends CommonOptions { + shell?: string | undefined; + signal?: AbortSignal | undefined; + maxBuffer?: number | undefined; + killSignal?: NodeJS.Signals | number | undefined; + encoding?: string | null | undefined; + } + interface ExecOptionsWithStringEncoding extends ExecOptions { + encoding?: BufferEncoding | undefined; + } + interface ExecOptionsWithBufferEncoding extends ExecOptions { + encoding: "buffer" | null; // specify `null`. + } + // TODO: Just Plain Wrong™ (see also nodejs/node#57392) + interface ExecException extends Error { + cmd?: string; + killed?: boolean; + code?: number; + signal?: NodeJS.Signals; + stdout?: string; + stderr?: string; + } + /** + * Spawns a shell then executes the `command` within that shell, buffering any + * generated output. The `command` string passed to the exec function is processed + * directly by the shell and special characters (vary based on [shell](https://en.wikipedia.org/wiki/List_of_command-line_interpreters)) + * need to be dealt with accordingly: + * + * ```js + * import { exec } from 'node:child_process'; + * + * exec('"/path/to/test file/test.sh" arg1 arg2'); + * // Double quotes are used so that the space in the path is not interpreted as + * // a delimiter of multiple arguments. + * + * exec('echo "The \\$HOME variable is $HOME"'); + * // The $HOME variable is escaped in the first instance, but not in the second. + * ``` + * + * **Never pass unsanitized user input to this function. Any input containing shell** + * **metacharacters may be used to trigger arbitrary command execution.** + * + * If a `callback` function is provided, it is called with the arguments `(error, stdout, stderr)`. On success, `error` will be `null`. On error, `error` will be an instance of `Error`. The + * `error.code` property will be + * the exit code of the process. By convention, any exit code other than `0` indicates an error. `error.signal` will be the signal that terminated the + * process. + * + * The `stdout` and `stderr` arguments passed to the callback will contain the + * stdout and stderr output of the child process. By default, Node.js will decode + * the output as UTF-8 and pass strings to the callback. The `encoding` option + * can be used to specify the character encoding used to decode the stdout and + * stderr output. If `encoding` is `'buffer'`, or an unrecognized character + * encoding, `Buffer` objects will be passed to the callback instead. + * + * ```js + * import { exec } from 'node:child_process'; + * exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => { + * if (error) { + * console.error(`exec error: ${error}`); + * return; + * } + * console.log(`stdout: ${stdout}`); + * console.error(`stderr: ${stderr}`); + * }); + * ``` + * + * If `timeout` is greater than `0`, the parent will send the signal + * identified by the `killSignal` property (the default is `'SIGTERM'`) if the + * child runs longer than `timeout` milliseconds. + * + * Unlike the [`exec(3)`](http://man7.org/linux/man-pages/man3/exec.3.html) POSIX system call, `child_process.exec()` does not replace + * the existing process and uses a shell to execute the command. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a `Promise` for an `Object` with `stdout` and `stderr` properties. The returned `ChildProcess` instance is attached to the `Promise` as a `child` property. In + * case of an error (including any error resulting in an exit code other than 0), a + * rejected promise is returned, with the same `error` object given in the + * callback, but with two additional properties `stdout` and `stderr`. + * + * ```js + * import util from 'node:util'; + * import child_process from 'node:child_process'; + * const exec = util.promisify(child_process.exec); + * + * async function lsExample() { + * const { stdout, stderr } = await exec('ls'); + * console.log('stdout:', stdout); + * console.error('stderr:', stderr); + * } + * lsExample(); + * ``` + * + * If the `signal` option is enabled, calling `.abort()` on the corresponding `AbortController` is similar to calling `.kill()` on the child process except + * the error passed to the callback will be an `AbortError`: + * + * ```js + * import { exec } from 'node:child_process'; + * const controller = new AbortController(); + * const { signal } = controller; + * const child = exec('grep ssh', { signal }, (error) => { + * console.error(error); // an AbortError + * }); + * controller.abort(); + * ``` + * @since v0.1.90 + * @param command The command to run, with space-separated arguments. + * @param callback called with the output when process terminates. + */ + function exec( + command: string, + callback?: (error: ExecException | null, stdout: string, stderr: string) => void, + ): ChildProcess; + // `options` with `"buffer"` or `null` for `encoding` means stdout/stderr are definitely `Buffer`. + function exec( + command: string, + options: ExecOptionsWithBufferEncoding, + callback?: (error: ExecException | null, stdout: NonSharedBuffer, stderr: NonSharedBuffer) => void, + ): ChildProcess; + // `options` with well-known or absent `encoding` means stdout/stderr are definitely `string`. + function exec( + command: string, + options: ExecOptionsWithStringEncoding, + callback?: (error: ExecException | null, stdout: string, stderr: string) => void, + ): ChildProcess; + // fallback if nothing else matches. Worst case is always `string | Buffer`. + function exec( + command: string, + options: ExecOptions | undefined | null, + callback?: ( + error: ExecException | null, + stdout: string | NonSharedBuffer, + stderr: string | NonSharedBuffer, + ) => void, + ): ChildProcess; + interface PromiseWithChild extends Promise { + child: ChildProcess; + } + namespace exec { + function __promisify__(command: string): PromiseWithChild<{ + stdout: string; + stderr: string; + }>; + function __promisify__( + command: string, + options: ExecOptionsWithBufferEncoding, + ): PromiseWithChild<{ + stdout: NonSharedBuffer; + stderr: NonSharedBuffer; + }>; + function __promisify__( + command: string, + options: ExecOptionsWithStringEncoding, + ): PromiseWithChild<{ + stdout: string; + stderr: string; + }>; + function __promisify__( + command: string, + options: ExecOptions | undefined | null, + ): PromiseWithChild<{ + stdout: string | NonSharedBuffer; + stderr: string | NonSharedBuffer; + }>; + } + interface ExecFileOptions extends CommonOptions, Abortable { + maxBuffer?: number | undefined; + killSignal?: NodeJS.Signals | number | undefined; + windowsVerbatimArguments?: boolean | undefined; + shell?: boolean | string | undefined; + signal?: AbortSignal | undefined; + encoding?: string | null | undefined; + } + interface ExecFileOptionsWithStringEncoding extends ExecFileOptions { + encoding?: BufferEncoding | undefined; + } + interface ExecFileOptionsWithBufferEncoding extends ExecFileOptions { + encoding: "buffer" | null; + } + /** @deprecated Use `ExecFileOptions` instead. */ + interface ExecFileOptionsWithOtherEncoding extends ExecFileOptions {} + // TODO: execFile exceptions can take many forms... this accurately describes none of them + type ExecFileException = + & Omit + & Omit + & { code?: string | number | null }; + /** + * The `child_process.execFile()` function is similar to {@link exec} except that it does not spawn a shell by default. Rather, the specified + * executable `file` is spawned directly as a new process making it slightly more + * efficient than {@link exec}. + * + * The same options as {@link exec} are supported. Since a shell is + * not spawned, behaviors such as I/O redirection and file globbing are not + * supported. + * + * ```js + * import { execFile } from 'node:child_process'; + * const child = execFile('node', ['--version'], (error, stdout, stderr) => { + * if (error) { + * throw error; + * } + * console.log(stdout); + * }); + * ``` + * + * The `stdout` and `stderr` arguments passed to the callback will contain the + * stdout and stderr output of the child process. By default, Node.js will decode + * the output as UTF-8 and pass strings to the callback. The `encoding` option + * can be used to specify the character encoding used to decode the stdout and + * stderr output. If `encoding` is `'buffer'`, or an unrecognized character + * encoding, `Buffer` objects will be passed to the callback instead. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a `Promise` for an `Object` with `stdout` and `stderr` properties. The returned `ChildProcess` instance is attached to the `Promise` as a `child` property. In + * case of an error (including any error resulting in an exit code other than 0), a + * rejected promise is returned, with the same `error` object given in the + * callback, but with two additional properties `stdout` and `stderr`. + * + * ```js + * import util from 'node:util'; + * import child_process from 'node:child_process'; + * const execFile = util.promisify(child_process.execFile); + * async function getVersion() { + * const { stdout } = await execFile('node', ['--version']); + * console.log(stdout); + * } + * getVersion(); + * ``` + * + * **If the `shell` option is enabled, do not pass unsanitized user input to this** + * **function. Any input containing shell metacharacters may be used to trigger** + * **arbitrary command execution.** + * + * If the `signal` option is enabled, calling `.abort()` on the corresponding `AbortController` is similar to calling `.kill()` on the child process except + * the error passed to the callback will be an `AbortError`: + * + * ```js + * import { execFile } from 'node:child_process'; + * const controller = new AbortController(); + * const { signal } = controller; + * const child = execFile('node', ['--version'], { signal }, (error) => { + * console.error(error); // an AbortError + * }); + * controller.abort(); + * ``` + * @since v0.1.91 + * @param file The name or path of the executable file to run. + * @param args List of string arguments. + * @param callback Called with the output when process terminates. + */ + // no `options` definitely means stdout/stderr are `string`. + function execFile( + file: string, + callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void, + ): ChildProcess; + function execFile( + file: string, + args: readonly string[] | undefined | null, + callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void, + ): ChildProcess; + // `options` with `"buffer"` or `null` for `encoding` means stdout/stderr are definitely `Buffer`. + function execFile( + file: string, + options: ExecFileOptionsWithBufferEncoding, + callback?: (error: ExecFileException | null, stdout: NonSharedBuffer, stderr: NonSharedBuffer) => void, + ): ChildProcess; + function execFile( + file: string, + args: readonly string[] | undefined | null, + options: ExecFileOptionsWithBufferEncoding, + callback?: (error: ExecFileException | null, stdout: NonSharedBuffer, stderr: NonSharedBuffer) => void, + ): ChildProcess; + // `options` with well-known or absent `encoding` means stdout/stderr are definitely `string`. + function execFile( + file: string, + options: ExecFileOptionsWithStringEncoding, + callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void, + ): ChildProcess; + function execFile( + file: string, + args: readonly string[] | undefined | null, + options: ExecFileOptionsWithStringEncoding, + callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void, + ): ChildProcess; + // fallback if nothing else matches. Worst case is always `string | Buffer`. + function execFile( + file: string, + options: ExecFileOptions | undefined | null, + callback: + | (( + error: ExecFileException | null, + stdout: string | NonSharedBuffer, + stderr: string | NonSharedBuffer, + ) => void) + | undefined + | null, + ): ChildProcess; + function execFile( + file: string, + args: readonly string[] | undefined | null, + options: ExecFileOptions | undefined | null, + callback: + | (( + error: ExecFileException | null, + stdout: string | NonSharedBuffer, + stderr: string | NonSharedBuffer, + ) => void) + | undefined + | null, + ): ChildProcess; + namespace execFile { + function __promisify__(file: string): PromiseWithChild<{ + stdout: string; + stderr: string; + }>; + function __promisify__( + file: string, + args: readonly string[] | undefined | null, + ): PromiseWithChild<{ + stdout: string; + stderr: string; + }>; + function __promisify__( + file: string, + options: ExecFileOptionsWithBufferEncoding, + ): PromiseWithChild<{ + stdout: NonSharedBuffer; + stderr: NonSharedBuffer; + }>; + function __promisify__( + file: string, + args: readonly string[] | undefined | null, + options: ExecFileOptionsWithBufferEncoding, + ): PromiseWithChild<{ + stdout: NonSharedBuffer; + stderr: NonSharedBuffer; + }>; + function __promisify__( + file: string, + options: ExecFileOptionsWithStringEncoding, + ): PromiseWithChild<{ + stdout: string; + stderr: string; + }>; + function __promisify__( + file: string, + args: readonly string[] | undefined | null, + options: ExecFileOptionsWithStringEncoding, + ): PromiseWithChild<{ + stdout: string; + stderr: string; + }>; + function __promisify__( + file: string, + options: ExecFileOptions | undefined | null, + ): PromiseWithChild<{ + stdout: string | NonSharedBuffer; + stderr: string | NonSharedBuffer; + }>; + function __promisify__( + file: string, + args: readonly string[] | undefined | null, + options: ExecFileOptions | undefined | null, + ): PromiseWithChild<{ + stdout: string | NonSharedBuffer; + stderr: string | NonSharedBuffer; + }>; + } + interface ForkOptions extends ProcessEnvOptions, MessagingOptions, Abortable { + execPath?: string | undefined; + execArgv?: string[] | undefined; + silent?: boolean | undefined; + /** + * Can be set to 'pipe', 'inherit', 'overlapped', or 'ignore', or an array of these strings. + * If passed as an array, the first element is used for `stdin`, the second for + * `stdout`, and the third for `stderr`. A fourth element can be used to + * specify the `stdio` behavior beyond the standard streams. See + * {@link ChildProcess.stdio} for more information. + * + * @default 'pipe' + */ + stdio?: StdioOptions | undefined; + detached?: boolean | undefined; + windowsVerbatimArguments?: boolean | undefined; + } + /** + * The `child_process.fork()` method is a special case of {@link spawn} used specifically to spawn new Node.js processes. + * Like {@link spawn}, a `ChildProcess` object is returned. The + * returned `ChildProcess` will have an additional communication channel + * built-in that allows messages to be passed back and forth between the parent and + * child. See `subprocess.send()` for details. + * + * Keep in mind that spawned Node.js child processes are + * independent of the parent with exception of the IPC communication channel + * that is established between the two. Each process has its own memory, with + * their own V8 instances. Because of the additional resource allocations + * required, spawning a large number of child Node.js processes is not + * recommended. + * + * By default, `child_process.fork()` will spawn new Node.js instances using the `process.execPath` of the parent process. The `execPath` property in the `options` object allows for an alternative + * execution path to be used. + * + * Node.js processes launched with a custom `execPath` will communicate with the + * parent process using the file descriptor (fd) identified using the + * environment variable `NODE_CHANNEL_FD` on the child process. + * + * Unlike the [`fork(2)`](http://man7.org/linux/man-pages/man2/fork.2.html) POSIX system call, `child_process.fork()` does not clone the + * current process. + * + * The `shell` option available in {@link spawn} is not supported by `child_process.fork()` and will be ignored if set. + * + * If the `signal` option is enabled, calling `.abort()` on the corresponding `AbortController` is similar to calling `.kill()` on the child process except + * the error passed to the callback will be an `AbortError`: + * + * ```js + * if (process.argv[2] === 'child') { + * setTimeout(() => { + * console.log(`Hello from ${process.argv[2]}!`); + * }, 1_000); + * } else { + * import { fork } from 'node:child_process'; + * const controller = new AbortController(); + * const { signal } = controller; + * const child = fork(__filename, ['child'], { signal }); + * child.on('error', (err) => { + * // This will be called with err being an AbortError if the controller aborts + * }); + * controller.abort(); // Stops the child process + * } + * ``` + * @since v0.5.0 + * @param modulePath The module to run in the child. + * @param args List of string arguments. + */ + function fork(modulePath: string | URL, options?: ForkOptions): ChildProcess; + function fork(modulePath: string | URL, args?: readonly string[], options?: ForkOptions): ChildProcess; + interface SpawnSyncOptions extends CommonSpawnOptions { + input?: string | NodeJS.ArrayBufferView | undefined; + maxBuffer?: number | undefined; + encoding?: BufferEncoding | "buffer" | null | undefined; + } + interface SpawnSyncOptionsWithStringEncoding extends SpawnSyncOptions { + encoding: BufferEncoding; + } + interface SpawnSyncOptionsWithBufferEncoding extends SpawnSyncOptions { + encoding?: "buffer" | null | undefined; + } + interface SpawnSyncReturns { + pid: number; + output: Array; + stdout: T; + stderr: T; + status: number | null; + signal: NodeJS.Signals | null; + error?: Error; + } + /** + * The `child_process.spawnSync()` method is generally identical to {@link spawn} with the exception that the function will not return + * until the child process has fully closed. When a timeout has been encountered + * and `killSignal` is sent, the method won't return until the process has + * completely exited. If the process intercepts and handles the `SIGTERM` signal + * and doesn't exit, the parent process will wait until the child process has + * exited. + * + * **If the `shell` option is enabled, do not pass unsanitized user input to this** + * **function. Any input containing shell metacharacters may be used to trigger** + * **arbitrary command execution.** + * @since v0.11.12 + * @param command The command to run. + * @param args List of string arguments. + */ + function spawnSync(command: string): SpawnSyncReturns; + function spawnSync(command: string, options: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns; + function spawnSync(command: string, options: SpawnSyncOptionsWithBufferEncoding): SpawnSyncReturns; + function spawnSync(command: string, options?: SpawnSyncOptions): SpawnSyncReturns; + function spawnSync(command: string, args: readonly string[]): SpawnSyncReturns; + function spawnSync( + command: string, + args: readonly string[], + options: SpawnSyncOptionsWithStringEncoding, + ): SpawnSyncReturns; + function spawnSync( + command: string, + args: readonly string[], + options: SpawnSyncOptionsWithBufferEncoding, + ): SpawnSyncReturns; + function spawnSync( + command: string, + args?: readonly string[], + options?: SpawnSyncOptions, + ): SpawnSyncReturns; + interface CommonExecOptions extends CommonOptions { + input?: string | NodeJS.ArrayBufferView | undefined; + /** + * Can be set to 'pipe', 'inherit, or 'ignore', or an array of these strings. + * If passed as an array, the first element is used for `stdin`, the second for + * `stdout`, and the third for `stderr`. A fourth element can be used to + * specify the `stdio` behavior beyond the standard streams. See + * {@link ChildProcess.stdio} for more information. + * + * @default 'pipe' + */ + stdio?: StdioOptions | undefined; + killSignal?: NodeJS.Signals | number | undefined; + maxBuffer?: number | undefined; + encoding?: BufferEncoding | "buffer" | null | undefined; + } + interface ExecSyncOptions extends CommonExecOptions { + shell?: string | undefined; + } + interface ExecSyncOptionsWithStringEncoding extends ExecSyncOptions { + encoding: BufferEncoding; + } + interface ExecSyncOptionsWithBufferEncoding extends ExecSyncOptions { + encoding?: "buffer" | null | undefined; + } + /** + * The `child_process.execSync()` method is generally identical to {@link exec} with the exception that the method will not return + * until the child process has fully closed. When a timeout has been encountered + * and `killSignal` is sent, the method won't return until the process has + * completely exited. If the child process intercepts and handles the `SIGTERM` signal and doesn't exit, the parent process will wait until the child process + * has exited. + * + * If the process times out or has a non-zero exit code, this method will throw. + * The `Error` object will contain the entire result from {@link spawnSync}. + * + * **Never pass unsanitized user input to this function. Any input containing shell** + * **metacharacters may be used to trigger arbitrary command execution.** + * @since v0.11.12 + * @param command The command to run. + * @return The stdout from the command. + */ + function execSync(command: string): NonSharedBuffer; + function execSync(command: string, options: ExecSyncOptionsWithStringEncoding): string; + function execSync(command: string, options: ExecSyncOptionsWithBufferEncoding): NonSharedBuffer; + function execSync(command: string, options?: ExecSyncOptions): string | NonSharedBuffer; + interface ExecFileSyncOptions extends CommonExecOptions { + shell?: boolean | string | undefined; + } + interface ExecFileSyncOptionsWithStringEncoding extends ExecFileSyncOptions { + encoding: BufferEncoding; + } + interface ExecFileSyncOptionsWithBufferEncoding extends ExecFileSyncOptions { + encoding?: "buffer" | null | undefined; // specify `null`. + } + /** + * The `child_process.execFileSync()` method is generally identical to {@link execFile} with the exception that the method will not + * return until the child process has fully closed. When a timeout has been + * encountered and `killSignal` is sent, the method won't return until the process + * has completely exited. + * + * If the child process intercepts and handles the `SIGTERM` signal and + * does not exit, the parent process will still wait until the child process has + * exited. + * + * If the process times out or has a non-zero exit code, this method will throw an `Error` that will include the full result of the underlying {@link spawnSync}. + * + * **If the `shell` option is enabled, do not pass unsanitized user input to this** + * **function. Any input containing shell metacharacters may be used to trigger** + * **arbitrary command execution.** + * @since v0.11.12 + * @param file The name or path of the executable file to run. + * @param args List of string arguments. + * @return The stdout from the command. + */ + function execFileSync(file: string): NonSharedBuffer; + function execFileSync(file: string, options: ExecFileSyncOptionsWithStringEncoding): string; + function execFileSync(file: string, options: ExecFileSyncOptionsWithBufferEncoding): NonSharedBuffer; + function execFileSync(file: string, options?: ExecFileSyncOptions): string | NonSharedBuffer; + function execFileSync(file: string, args: readonly string[]): NonSharedBuffer; + function execFileSync( + file: string, + args: readonly string[], + options: ExecFileSyncOptionsWithStringEncoding, + ): string; + function execFileSync( + file: string, + args: readonly string[], + options: ExecFileSyncOptionsWithBufferEncoding, + ): NonSharedBuffer; + function execFileSync( + file: string, + args?: readonly string[], + options?: ExecFileSyncOptions, + ): string | NonSharedBuffer; +} +declare module "node:child_process" { + export * from "child_process"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/cluster.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/cluster.d.ts new file mode 100644 index 00000000..eab9783a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/cluster.d.ts @@ -0,0 +1,578 @@ +/** + * Clusters of Node.js processes can be used to run multiple instances of Node.js + * that can distribute workloads among their application threads. When process isolation + * is not needed, use the [`worker_threads`](https://nodejs.org/docs/latest-v22.x/api/worker_threads.html) + * module instead, which allows running multiple application threads within a single Node.js instance. + * + * The cluster module allows easy creation of child processes that all share + * server ports. + * + * ```js + * import cluster from 'node:cluster'; + * import http from 'node:http'; + * import { availableParallelism } from 'node:os'; + * import process from 'node:process'; + * + * const numCPUs = availableParallelism(); + * + * if (cluster.isPrimary) { + * console.log(`Primary ${process.pid} is running`); + * + * // Fork workers. + * for (let i = 0; i < numCPUs; i++) { + * cluster.fork(); + * } + * + * cluster.on('exit', (worker, code, signal) => { + * console.log(`worker ${worker.process.pid} died`); + * }); + * } else { + * // Workers can share any TCP connection + * // In this case it is an HTTP server + * http.createServer((req, res) => { + * res.writeHead(200); + * res.end('hello world\n'); + * }).listen(8000); + * + * console.log(`Worker ${process.pid} started`); + * } + * ``` + * + * Running Node.js will now share port 8000 between the workers: + * + * ```console + * $ node server.js + * Primary 3596 is running + * Worker 4324 started + * Worker 4520 started + * Worker 6056 started + * Worker 5644 started + * ``` + * + * On Windows, it is not yet possible to set up a named pipe server in a worker. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/cluster.js) + */ +declare module "cluster" { + import * as child from "node:child_process"; + import EventEmitter = require("node:events"); + import * as net from "node:net"; + type SerializationType = "json" | "advanced"; + export interface ClusterSettings { + /** + * List of string arguments passed to the Node.js executable. + * @default process.execArgv + */ + execArgv?: string[] | undefined; + /** + * File path to worker file. + * @default process.argv[1] + */ + exec?: string | undefined; + /** + * String arguments passed to worker. + * @default process.argv.slice(2) + */ + args?: readonly string[] | undefined; + /** + * Whether or not to send output to parent's stdio. + * @default false + */ + silent?: boolean | undefined; + /** + * Configures the stdio of forked processes. Because the cluster module relies on IPC to function, this configuration must + * contain an `'ipc'` entry. When this option is provided, it overrides `silent`. See [`child_prcess.spawn()`](https://nodejs.org/docs/latest-v22.x/api/child_process.html#child_processspawncommand-args-options)'s + * [`stdio`](https://nodejs.org/docs/latest-v22.x/api/child_process.html#optionsstdio). + */ + stdio?: any[] | undefined; + /** + * Sets the user identity of the process. (See [`setuid(2)`](https://man7.org/linux/man-pages/man2/setuid.2.html).) + */ + uid?: number | undefined; + /** + * Sets the group identity of the process. (See [`setgid(2)`](https://man7.org/linux/man-pages/man2/setgid.2.html).) + */ + gid?: number | undefined; + /** + * Sets inspector port of worker. This can be a number, or a function that takes no arguments and returns a number. + * By default each worker gets its own port, incremented from the primary's `process.debugPort`. + */ + inspectPort?: number | (() => number) | undefined; + /** + * Specify the kind of serialization used for sending messages between processes. Possible values are `'json'` and `'advanced'`. + * See [Advanced serialization for `child_process`](https://nodejs.org/docs/latest-v22.x/api/child_process.html#advanced-serialization) for more details. + * @default false + */ + serialization?: SerializationType | undefined; + /** + * Current working directory of the worker process. + * @default undefined (inherits from parent process) + */ + cwd?: string | undefined; + /** + * Hide the forked processes console window that would normally be created on Windows systems. + * @default false + */ + windowsHide?: boolean | undefined; + } + export interface Address { + address: string; + port: number; + /** + * The `addressType` is one of: + * + * * `4` (TCPv4) + * * `6` (TCPv6) + * * `-1` (Unix domain socket) + * * `'udp4'` or `'udp6'` (UDPv4 or UDPv6) + */ + addressType: 4 | 6 | -1 | "udp4" | "udp6"; + } + /** + * A `Worker` object contains all public information and method about a worker. + * In the primary it can be obtained using `cluster.workers`. In a worker + * it can be obtained using `cluster.worker`. + * @since v0.7.0 + */ + export class Worker extends EventEmitter { + /** + * Each new worker is given its own unique id, this id is stored in the `id`. + * + * While a worker is alive, this is the key that indexes it in `cluster.workers`. + * @since v0.8.0 + */ + id: number; + /** + * All workers are created using [`child_process.fork()`](https://nodejs.org/docs/latest-v22.x/api/child_process.html#child_processforkmodulepath-args-options), the returned object + * from this function is stored as `.process`. In a worker, the global `process` is stored. + * + * See: [Child Process module](https://nodejs.org/docs/latest-v22.x/api/child_process.html#child_processforkmodulepath-args-options). + * + * Workers will call `process.exit(0)` if the `'disconnect'` event occurs + * on `process` and `.exitedAfterDisconnect` is not `true`. This protects against + * accidental disconnection. + * @since v0.7.0 + */ + process: child.ChildProcess; + /** + * Send a message to a worker or primary, optionally with a handle. + * + * In the primary, this sends a message to a specific worker. It is identical to [`ChildProcess.send()`](https://nodejs.org/docs/latest-v22.x/api/child_process.html#subprocesssendmessage-sendhandle-options-callback). + * + * In a worker, this sends a message to the primary. It is identical to `process.send()`. + * + * This example will echo back all messages from the primary: + * + * ```js + * if (cluster.isPrimary) { + * const worker = cluster.fork(); + * worker.send('hi there'); + * + * } else if (cluster.isWorker) { + * process.on('message', (msg) => { + * process.send(msg); + * }); + * } + * ``` + * @since v0.7.0 + * @param options The `options` argument, if present, is an object used to parameterize the sending of certain types of handles. + */ + send(message: child.Serializable, callback?: (error: Error | null) => void): boolean; + send( + message: child.Serializable, + sendHandle: child.SendHandle, + callback?: (error: Error | null) => void, + ): boolean; + send( + message: child.Serializable, + sendHandle: child.SendHandle, + options?: child.MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + /** + * This function will kill the worker. In the primary worker, it does this by + * disconnecting the `worker.process`, and once disconnected, killing with `signal`. In the worker, it does it by killing the process with `signal`. + * + * The `kill()` function kills the worker process without waiting for a graceful + * disconnect, it has the same behavior as `worker.process.kill()`. + * + * This method is aliased as `worker.destroy()` for backwards compatibility. + * + * In a worker, `process.kill()` exists, but it is not this function; + * it is [`kill()`](https://nodejs.org/docs/latest-v22.x/api/process.html#processkillpid-signal). + * @since v0.9.12 + * @param [signal='SIGTERM'] Name of the kill signal to send to the worker process. + */ + kill(signal?: string): void; + destroy(signal?: string): void; + /** + * In a worker, this function will close all servers, wait for the `'close'` event + * on those servers, and then disconnect the IPC channel. + * + * In the primary, an internal message is sent to the worker causing it to call `.disconnect()` on itself. + * + * Causes `.exitedAfterDisconnect` to be set. + * + * After a server is closed, it will no longer accept new connections, + * but connections may be accepted by any other listening worker. Existing + * connections will be allowed to close as usual. When no more connections exist, + * see `server.close()`, the IPC channel to the worker will close allowing it + * to die gracefully. + * + * The above applies _only_ to server connections, client connections are not + * automatically closed by workers, and disconnect does not wait for them to close + * before exiting. + * + * In a worker, `process.disconnect` exists, but it is not this function; + * it is `disconnect()`. + * + * Because long living server connections may block workers from disconnecting, it + * may be useful to send a message, so application specific actions may be taken to + * close them. It also may be useful to implement a timeout, killing a worker if + * the `'disconnect'` event has not been emitted after some time. + * + * ```js + * import net from 'node:net'; + * + * if (cluster.isPrimary) { + * const worker = cluster.fork(); + * let timeout; + * + * worker.on('listening', (address) => { + * worker.send('shutdown'); + * worker.disconnect(); + * timeout = setTimeout(() => { + * worker.kill(); + * }, 2000); + * }); + * + * worker.on('disconnect', () => { + * clearTimeout(timeout); + * }); + * + * } else if (cluster.isWorker) { + * const server = net.createServer((socket) => { + * // Connections never end + * }); + * + * server.listen(8000); + * + * process.on('message', (msg) => { + * if (msg === 'shutdown') { + * // Initiate graceful close of any connections to server + * } + * }); + * } + * ``` + * @since v0.7.7 + * @return A reference to `worker`. + */ + disconnect(): this; + /** + * This function returns `true` if the worker is connected to its primary via its + * IPC channel, `false` otherwise. A worker is connected to its primary after it + * has been created. It is disconnected after the `'disconnect'` event is emitted. + * @since v0.11.14 + */ + isConnected(): boolean; + /** + * This function returns `true` if the worker's process has terminated (either + * because of exiting or being signaled). Otherwise, it returns `false`. + * + * ```js + * import cluster from 'node:cluster'; + * import http from 'node:http'; + * import { availableParallelism } from 'node:os'; + * import process from 'node:process'; + * + * const numCPUs = availableParallelism(); + * + * if (cluster.isPrimary) { + * console.log(`Primary ${process.pid} is running`); + * + * // Fork workers. + * for (let i = 0; i < numCPUs; i++) { + * cluster.fork(); + * } + * + * cluster.on('fork', (worker) => { + * console.log('worker is dead:', worker.isDead()); + * }); + * + * cluster.on('exit', (worker, code, signal) => { + * console.log('worker is dead:', worker.isDead()); + * }); + * } else { + * // Workers can share any TCP connection. In this case, it is an HTTP server. + * http.createServer((req, res) => { + * res.writeHead(200); + * res.end(`Current process\n ${process.pid}`); + * process.kill(process.pid); + * }).listen(8000); + * } + * ``` + * @since v0.11.14 + */ + isDead(): boolean; + /** + * This property is `true` if the worker exited due to `.disconnect()`. + * If the worker exited any other way, it is `false`. If the + * worker has not exited, it is `undefined`. + * + * The boolean `worker.exitedAfterDisconnect` allows distinguishing between + * voluntary and accidental exit, the primary may choose not to respawn a worker + * based on this value. + * + * ```js + * cluster.on('exit', (worker, code, signal) => { + * if (worker.exitedAfterDisconnect === true) { + * console.log('Oh, it was just voluntary – no need to worry'); + * } + * }); + * + * // kill worker + * worker.kill(); + * ``` + * @since v6.0.0 + */ + exitedAfterDisconnect: boolean; + /** + * events.EventEmitter + * 1. disconnect + * 2. error + * 3. exit + * 4. listening + * 5. message + * 6. online + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "disconnect", listener: () => void): this; + addListener(event: "error", listener: (error: Error) => void): this; + addListener(event: "exit", listener: (code: number, signal: string) => void): this; + addListener(event: "listening", listener: (address: Address) => void): this; + addListener(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined. + addListener(event: "online", listener: () => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "disconnect"): boolean; + emit(event: "error", error: Error): boolean; + emit(event: "exit", code: number, signal: string): boolean; + emit(event: "listening", address: Address): boolean; + emit(event: "message", message: any, handle: net.Socket | net.Server): boolean; + emit(event: "online"): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "disconnect", listener: () => void): this; + on(event: "error", listener: (error: Error) => void): this; + on(event: "exit", listener: (code: number, signal: string) => void): this; + on(event: "listening", listener: (address: Address) => void): this; + on(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined. + on(event: "online", listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "disconnect", listener: () => void): this; + once(event: "error", listener: (error: Error) => void): this; + once(event: "exit", listener: (code: number, signal: string) => void): this; + once(event: "listening", listener: (address: Address) => void): this; + once(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined. + once(event: "online", listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "disconnect", listener: () => void): this; + prependListener(event: "error", listener: (error: Error) => void): this; + prependListener(event: "exit", listener: (code: number, signal: string) => void): this; + prependListener(event: "listening", listener: (address: Address) => void): this; + prependListener(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined. + prependListener(event: "online", listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "disconnect", listener: () => void): this; + prependOnceListener(event: "error", listener: (error: Error) => void): this; + prependOnceListener(event: "exit", listener: (code: number, signal: string) => void): this; + prependOnceListener(event: "listening", listener: (address: Address) => void): this; + prependOnceListener(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined. + prependOnceListener(event: "online", listener: () => void): this; + } + export interface Cluster extends EventEmitter { + disconnect(callback?: () => void): void; + /** + * Spawn a new worker process. + * + * This can only be called from the primary process. + * @param env Key/value pairs to add to worker process environment. + * @since v0.6.0 + */ + fork(env?: any): Worker; + /** @deprecated since v16.0.0 - use isPrimary. */ + readonly isMaster: boolean; + /** + * True if the process is a primary. This is determined by the `process.env.NODE_UNIQUE_ID`. If `process.env.NODE_UNIQUE_ID` + * is undefined, then `isPrimary` is `true`. + * @since v16.0.0 + */ + readonly isPrimary: boolean; + /** + * True if the process is not a primary (it is the negation of `cluster.isPrimary`). + * @since v0.6.0 + */ + readonly isWorker: boolean; + /** + * The scheduling policy, either `cluster.SCHED_RR` for round-robin or `cluster.SCHED_NONE` to leave it to the operating system. This is a + * global setting and effectively frozen once either the first worker is spawned, or [`.setupPrimary()`](https://nodejs.org/docs/latest-v22.x/api/cluster.html#clustersetupprimarysettings) + * is called, whichever comes first. + * + * `SCHED_RR` is the default on all operating systems except Windows. Windows will change to `SCHED_RR` once libuv is able to effectively distribute + * IOCP handles without incurring a large performance hit. + * + * `cluster.schedulingPolicy` can also be set through the `NODE_CLUSTER_SCHED_POLICY` environment variable. Valid values are `'rr'` and `'none'`. + * @since v0.11.2 + */ + schedulingPolicy: number; + /** + * After calling [`.setupPrimary()`](https://nodejs.org/docs/latest-v22.x/api/cluster.html#clustersetupprimarysettings) + * (or [`.fork()`](https://nodejs.org/docs/latest-v22.x/api/cluster.html#clusterforkenv)) this settings object will contain + * the settings, including the default values. + * + * This object is not intended to be changed or set manually. + * @since v0.7.1 + */ + readonly settings: ClusterSettings; + /** @deprecated since v16.0.0 - use [`.setupPrimary()`](https://nodejs.org/docs/latest-v22.x/api/cluster.html#clustersetupprimarysettings) instead. */ + setupMaster(settings?: ClusterSettings): void; + /** + * `setupPrimary` is used to change the default 'fork' behavior. Once called, the settings will be present in `cluster.settings`. + * + * Any settings changes only affect future calls to [`.fork()`](https://nodejs.org/docs/latest-v22.x/api/cluster.html#clusterforkenv) + * and have no effect on workers that are already running. + * + * The only attribute of a worker that cannot be set via `.setupPrimary()` is the `env` passed to + * [`.fork()`](https://nodejs.org/docs/latest-v22.x/api/cluster.html#clusterforkenv). + * + * The defaults above apply to the first call only; the defaults for later calls are the current values at the time of + * `cluster.setupPrimary()` is called. + * + * ```js + * import cluster from 'node:cluster'; + * + * cluster.setupPrimary({ + * exec: 'worker.js', + * args: ['--use', 'https'], + * silent: true, + * }); + * cluster.fork(); // https worker + * cluster.setupPrimary({ + * exec: 'worker.js', + * args: ['--use', 'http'], + * }); + * cluster.fork(); // http worker + * ``` + * + * This can only be called from the primary process. + * @since v16.0.0 + */ + setupPrimary(settings?: ClusterSettings): void; + /** + * A reference to the current worker object. Not available in the primary process. + * + * ```js + * import cluster from 'node:cluster'; + * + * if (cluster.isPrimary) { + * console.log('I am primary'); + * cluster.fork(); + * cluster.fork(); + * } else if (cluster.isWorker) { + * console.log(`I am worker #${cluster.worker.id}`); + * } + * ``` + * @since v0.7.0 + */ + readonly worker?: Worker; + /** + * A hash that stores the active worker objects, keyed by `id` field. This makes it easy to loop through all the workers. It is only available in the primary process. + * + * A worker is removed from `cluster.workers` after the worker has disconnected _and_ exited. The order between these two events cannot be determined in advance. However, it + * is guaranteed that the removal from the `cluster.workers` list happens before the last `'disconnect'` or `'exit'` event is emitted. + * + * ```js + * import cluster from 'node:cluster'; + * + * for (const worker of Object.values(cluster.workers)) { + * worker.send('big announcement to all workers'); + * } + * ``` + * @since v0.7.0 + */ + readonly workers?: NodeJS.Dict; + readonly SCHED_NONE: number; + readonly SCHED_RR: number; + /** + * events.EventEmitter + * 1. disconnect + * 2. exit + * 3. fork + * 4. listening + * 5. message + * 6. online + * 7. setup + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "disconnect", listener: (worker: Worker) => void): this; + addListener(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this; + addListener(event: "fork", listener: (worker: Worker) => void): this; + addListener(event: "listening", listener: (worker: Worker, address: Address) => void): this; + addListener( + event: "message", + listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void, + ): this; // the handle is a net.Socket or net.Server object, or undefined. + addListener(event: "online", listener: (worker: Worker) => void): this; + addListener(event: "setup", listener: (settings: ClusterSettings) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "disconnect", worker: Worker): boolean; + emit(event: "exit", worker: Worker, code: number, signal: string): boolean; + emit(event: "fork", worker: Worker): boolean; + emit(event: "listening", worker: Worker, address: Address): boolean; + emit(event: "message", worker: Worker, message: any, handle: net.Socket | net.Server): boolean; + emit(event: "online", worker: Worker): boolean; + emit(event: "setup", settings: ClusterSettings): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "disconnect", listener: (worker: Worker) => void): this; + on(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this; + on(event: "fork", listener: (worker: Worker) => void): this; + on(event: "listening", listener: (worker: Worker, address: Address) => void): this; + on(event: "message", listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined. + on(event: "online", listener: (worker: Worker) => void): this; + on(event: "setup", listener: (settings: ClusterSettings) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "disconnect", listener: (worker: Worker) => void): this; + once(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this; + once(event: "fork", listener: (worker: Worker) => void): this; + once(event: "listening", listener: (worker: Worker, address: Address) => void): this; + once(event: "message", listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined. + once(event: "online", listener: (worker: Worker) => void): this; + once(event: "setup", listener: (settings: ClusterSettings) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "disconnect", listener: (worker: Worker) => void): this; + prependListener(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this; + prependListener(event: "fork", listener: (worker: Worker) => void): this; + prependListener(event: "listening", listener: (worker: Worker, address: Address) => void): this; + prependListener( + event: "message", + listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void, + ): this; + prependListener(event: "online", listener: (worker: Worker) => void): this; + prependListener(event: "setup", listener: (settings: ClusterSettings) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "disconnect", listener: (worker: Worker) => void): this; + prependOnceListener(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this; + prependOnceListener(event: "fork", listener: (worker: Worker) => void): this; + prependOnceListener(event: "listening", listener: (worker: Worker, address: Address) => void): this; + // the handle is a net.Socket or net.Server object, or undefined. + prependOnceListener( + event: "message", + listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void, + ): this; + prependOnceListener(event: "online", listener: (worker: Worker) => void): this; + prependOnceListener(event: "setup", listener: (settings: ClusterSettings) => void): this; + } + const cluster: Cluster; + export default cluster; +} +declare module "node:cluster" { + export * from "cluster"; + export { default as default } from "cluster"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/disposable.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/disposable.d.ts new file mode 100644 index 00000000..e23d5a7c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/disposable.d.ts @@ -0,0 +1,14 @@ +// Polyfills for the explicit resource management types added in TypeScript 5.2. + +interface SymbolConstructor { + readonly dispose: unique symbol; + readonly asyncDispose: unique symbol; +} + +interface Disposable { + [Symbol.dispose](): void; +} + +interface AsyncDisposable { + [Symbol.asyncDispose](): PromiseLike; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/index.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/index.d.ts new file mode 100644 index 00000000..5c41e372 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/index.d.ts @@ -0,0 +1,9 @@ +// Declaration files in this directory contain types relating to TypeScript library features +// that are not included in all TypeScript versions supported by DefinitelyTyped, but +// which can be made backwards-compatible without needing `typesVersions`. +// If adding declarations to this directory, please specify which versions of TypeScript require them, +// so that they can be removed when no longer needed. + +/// +/// +/// diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/indexable.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/indexable.d.ts new file mode 100644 index 00000000..262ba09c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/indexable.d.ts @@ -0,0 +1,20 @@ +// Polyfill for ES2022's .at() method on string/array prototypes, added to TypeScript in 4.6. + +interface RelativeIndexable { + at(index: number): T | undefined; +} + +interface String extends RelativeIndexable {} +interface Array extends RelativeIndexable {} +interface ReadonlyArray extends RelativeIndexable {} +interface Int8Array extends RelativeIndexable {} +interface Uint8Array extends RelativeIndexable {} +interface Uint8ClampedArray extends RelativeIndexable {} +interface Int16Array extends RelativeIndexable {} +interface Uint16Array extends RelativeIndexable {} +interface Int32Array extends RelativeIndexable {} +interface Uint32Array extends RelativeIndexable {} +interface Float32Array extends RelativeIndexable {} +interface Float64Array extends RelativeIndexable {} +interface BigInt64Array extends RelativeIndexable {} +interface BigUint64Array extends RelativeIndexable {} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/iterators.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/iterators.d.ts new file mode 100644 index 00000000..2f9be9cd --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/compatibility/iterators.d.ts @@ -0,0 +1,20 @@ +// Backwards-compatible iterator interfaces, augmented with iterator helper methods by lib.esnext.iterator in TypeScript 5.6. +// The IterableIterator interface does not contain these methods, which creates assignability issues in places where IteratorObjects +// are expected (eg. DOM-compatible APIs) if lib.esnext.iterator is loaded. +// Also ensures that iterators returned by the Node API, which inherit from Iterator.prototype, correctly expose the iterator helper methods +// if lib.esnext.iterator is loaded. + +// Placeholders for TS <5.6 +interface IteratorObject {} +interface AsyncIteratorObject {} + +declare namespace NodeJS { + // Populate iterator methods for TS <5.6 + interface Iterator extends globalThis.Iterator {} + interface AsyncIterator extends globalThis.AsyncIterator {} + + // Polyfill for TS 5.6's instrinsic BuiltinIteratorReturn type, required for DOM-compatible iterators + type BuiltinIteratorReturn = ReturnType extends + globalThis.Iterator ? TReturn + : any; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/console.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/console.d.ts new file mode 100644 index 00000000..3e4c2d9a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/console.d.ts @@ -0,0 +1,452 @@ +/** + * The `node:console` module provides a simple debugging console that is similar to + * the JavaScript console mechanism provided by web browsers. + * + * The module exports two specific components: + * + * * A `Console` class with methods such as `console.log()`, `console.error()`, and `console.warn()` that can be used to write to any Node.js stream. + * * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and + * [`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. + * + * _**Warning**_: The global console object's methods are neither consistently + * synchronous like the browser APIs they resemble, nor are they consistently + * asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for + * more information. + * + * Example using the global `console`: + * + * ```js + * console.log('hello world'); + * // Prints: hello world, to stdout + * console.log('hello %s', 'world'); + * // Prints: hello world, to stdout + * console.error(new Error('Whoops, something bad happened')); + * // Prints error message and stack trace to stderr: + * // Error: Whoops, something bad happened + * // at [eval]:5:15 + * // at Script.runInThisContext (node:vm:132:18) + * // at Object.runInThisContext (node:vm:309:38) + * // at node:internal/process/execution:77:19 + * // at [eval]-wrapper:6:22 + * // at evalScript (node:internal/process/execution:76:60) + * // at node:internal/main/eval_string:23:3 + * + * const name = 'Will Robinson'; + * console.warn(`Danger ${name}! Danger!`); + * // Prints: Danger Will Robinson! Danger!, to stderr + * ``` + * + * Example using the `Console` class: + * + * ```js + * const out = getStreamSomehow(); + * const err = getStreamSomehow(); + * const myConsole = new console.Console(out, err); + * + * myConsole.log('hello world'); + * // Prints: hello world, to out + * myConsole.log('hello %s', 'world'); + * // Prints: hello world, to out + * myConsole.error(new Error('Whoops, something bad happened')); + * // Prints: [Error: Whoops, something bad happened], to err + * + * const name = 'Will Robinson'; + * myConsole.warn(`Danger ${name}! Danger!`); + * // Prints: Danger Will Robinson! Danger!, to err + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/console.js) + */ +declare module "console" { + import console = require("node:console"); + export = console; +} +declare module "node:console" { + import { InspectOptions } from "node:util"; + global { + // This needs to be global to avoid TS2403 in case lib.dom.d.ts is present in the same build + interface Console { + Console: console.ConsoleConstructor; + /** + * `console.assert()` writes a message if `value` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) or omitted. It only + * writes a message and does not otherwise affect execution. The output always + * starts with `"Assertion failed"`. If provided, `message` is formatted using + * [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args). + * + * If `value` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), nothing happens. + * + * ```js + * console.assert(true, 'does nothing'); + * + * console.assert(false, 'Whoops %s work', 'didn\'t'); + * // Assertion failed: Whoops didn't work + * + * console.assert(); + * // Assertion failed + * ``` + * @since v0.1.101 + * @param value The value tested for being truthy. + * @param message All arguments besides `value` are used as error message. + */ + assert(value: any, message?: string, ...optionalParams: any[]): void; + /** + * When `stdout` is a TTY, calling `console.clear()` will attempt to clear the + * TTY. When `stdout` is not a TTY, this method does nothing. + * + * The specific operation of `console.clear()` can vary across operating systems + * and terminal types. For most Linux operating systems, `console.clear()` operates similarly to the `clear` shell command. On Windows, `console.clear()` will clear only the output in the + * current terminal viewport for the Node.js + * binary. + * @since v8.3.0 + */ + clear(): void; + /** + * Maintains an internal counter specific to `label` and outputs to `stdout` the + * number of times `console.count()` has been called with the given `label`. + * + * ```js + * > console.count() + * default: 1 + * undefined + * > console.count('default') + * default: 2 + * undefined + * > console.count('abc') + * abc: 1 + * undefined + * > console.count('xyz') + * xyz: 1 + * undefined + * > console.count('abc') + * abc: 2 + * undefined + * > console.count() + * default: 3 + * undefined + * > + * ``` + * @since v8.3.0 + * @param [label='default'] The display label for the counter. + */ + count(label?: string): void; + /** + * Resets the internal counter specific to `label`. + * + * ```js + * > console.count('abc'); + * abc: 1 + * undefined + * > console.countReset('abc'); + * undefined + * > console.count('abc'); + * abc: 1 + * undefined + * > + * ``` + * @since v8.3.0 + * @param [label='default'] The display label for the counter. + */ + countReset(label?: string): void; + /** + * The `console.debug()` function is an alias for {@link log}. + * @since v8.0.0 + */ + debug(message?: any, ...optionalParams: any[]): void; + /** + * Uses [`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options) on `obj` and prints the resulting string to `stdout`. + * This function bypasses any custom `inspect()` function defined on `obj`. + * @since v0.1.101 + */ + dir(obj: any, options?: InspectOptions): void; + /** + * This method calls `console.log()` passing it the arguments received. + * This method does not produce any XML formatting. + * @since v8.0.0 + */ + dirxml(...data: any[]): void; + /** + * Prints to `stderr` with newline. Multiple arguments can be passed, with the + * first used as the primary message and all additional used as substitution + * values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) + * (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)). + * + * ```js + * const code = 5; + * console.error('error #%d', code); + * // Prints: error #5, to stderr + * console.error('error', code); + * // Prints: error 5, to stderr + * ``` + * + * If formatting elements (e.g. `%d`) are not found in the first string then + * [`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options) is called on each argument and the + * resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) + * for more information. + * @since v0.1.100 + */ + error(message?: any, ...optionalParams: any[]): void; + /** + * Increases indentation of subsequent lines by spaces for `groupIndentation` length. + * + * If one or more `label`s are provided, those are printed first without the + * additional indentation. + * @since v8.5.0 + */ + group(...label: any[]): void; + /** + * An alias for {@link group}. + * @since v8.5.0 + */ + groupCollapsed(...label: any[]): void; + /** + * Decreases indentation of subsequent lines by spaces for `groupIndentation` length. + * @since v8.5.0 + */ + groupEnd(): void; + /** + * The `console.info()` function is an alias for {@link log}. + * @since v0.1.100 + */ + info(message?: any, ...optionalParams: any[]): void; + /** + * Prints to `stdout` with newline. Multiple arguments can be passed, with the + * first used as the primary message and all additional used as substitution + * values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) + * (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)). + * + * ```js + * const count = 5; + * console.log('count: %d', count); + * // Prints: count: 5, to stdout + * console.log('count:', count); + * // Prints: count: 5, to stdout + * ``` + * + * See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information. + * @since v0.1.100 + */ + log(message?: any, ...optionalParams: any[]): void; + /** + * Try to construct a table with the columns of the properties of `tabularData` (or use `properties`) and rows of `tabularData` and log it. Falls back to just + * logging the argument if it can't be parsed as tabular. + * + * ```js + * // These can't be parsed as tabular data + * console.table(Symbol()); + * // Symbol() + * + * console.table(undefined); + * // undefined + * + * console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }]); + * // ┌─────────┬─────┬─────┐ + * // │ (index) │ a │ b │ + * // ├─────────┼─────┼─────┤ + * // │ 0 │ 1 │ 'Y' │ + * // │ 1 │ 'Z' │ 2 │ + * // └─────────┴─────┴─────┘ + * + * console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }], ['a']); + * // ┌─────────┬─────┐ + * // │ (index) │ a │ + * // ├─────────┼─────┤ + * // │ 0 │ 1 │ + * // │ 1 │ 'Z' │ + * // └─────────┴─────┘ + * ``` + * @since v10.0.0 + * @param properties Alternate properties for constructing the table. + */ + table(tabularData: any, properties?: readonly string[]): void; + /** + * Starts a timer that can be used to compute the duration of an operation. Timers + * are identified by a unique `label`. Use the same `label` when calling {@link timeEnd} to stop the timer and output the elapsed time in + * suitable time units to `stdout`. For example, if the elapsed + * time is 3869ms, `console.timeEnd()` displays "3.869s". + * @since v0.1.104 + * @param [label='default'] + */ + time(label?: string): void; + /** + * Stops a timer that was previously started by calling {@link time} and + * prints the result to `stdout`: + * + * ```js + * console.time('bunch-of-stuff'); + * // Do a bunch of stuff. + * console.timeEnd('bunch-of-stuff'); + * // Prints: bunch-of-stuff: 225.438ms + * ``` + * @since v0.1.104 + * @param [label='default'] + */ + timeEnd(label?: string): void; + /** + * For a timer that was previously started by calling {@link time}, prints + * the elapsed time and other `data` arguments to `stdout`: + * + * ```js + * console.time('process'); + * const value = expensiveProcess1(); // Returns 42 + * console.timeLog('process', value); + * // Prints "process: 365.227ms 42". + * doExpensiveProcess2(value); + * console.timeEnd('process'); + * ``` + * @since v10.7.0 + * @param [label='default'] + */ + timeLog(label?: string, ...data: any[]): void; + /** + * Prints to `stderr` the string `'Trace: '`, followed by the [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) + * formatted message and stack trace to the current position in the code. + * + * ```js + * console.trace('Show me'); + * // Prints: (stack trace will vary based on where trace is called) + * // Trace: Show me + * // at repl:2:9 + * // at REPLServer.defaultEval (repl.js:248:27) + * // at bound (domain.js:287:14) + * // at REPLServer.runBound [as eval] (domain.js:300:12) + * // at REPLServer. (repl.js:412:12) + * // at emitOne (events.js:82:20) + * // at REPLServer.emit (events.js:169:7) + * // at REPLServer.Interface._onLine (readline.js:210:10) + * // at REPLServer.Interface._line (readline.js:549:8) + * // at REPLServer.Interface._ttyWrite (readline.js:826:14) + * ``` + * @since v0.1.104 + */ + trace(message?: any, ...optionalParams: any[]): void; + /** + * The `console.warn()` function is an alias for {@link error}. + * @since v0.1.100 + */ + warn(message?: any, ...optionalParams: any[]): void; + // --- Inspector mode only --- + /** + * This method does not display anything unless used in the inspector. The `console.profile()` + * method starts a JavaScript CPU profile with an optional label until {@link profileEnd} + * is called. The profile is then added to the Profile panel of the inspector. + * + * ```js + * console.profile('MyLabel'); + * // Some code + * console.profileEnd('MyLabel'); + * // Adds the profile 'MyLabel' to the Profiles panel of the inspector. + * ``` + * @since v8.0.0 + */ + profile(label?: string): void; + /** + * This method does not display anything unless used in the inspector. Stops the current + * JavaScript CPU profiling session if one has been started and prints the report to the + * Profiles panel of the inspector. See {@link profile} for an example. + * + * If this method is called without a label, the most recently started profile is stopped. + * @since v8.0.0 + */ + profileEnd(label?: string): void; + /** + * This method does not display anything unless used in the inspector. The `console.timeStamp()` + * method adds an event with the label `'label'` to the Timeline panel of the inspector. + * @since v8.0.0 + */ + timeStamp(label?: string): void; + } + /** + * The `console` module provides a simple debugging console that is similar to the + * JavaScript console mechanism provided by web browsers. + * + * The module exports two specific components: + * + * * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. + * * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and + * [`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. + * + * _**Warning**_: The global console object's methods are neither consistently + * synchronous like the browser APIs they resemble, nor are they consistently + * asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for + * more information. + * + * Example using the global `console`: + * + * ```js + * console.log('hello world'); + * // Prints: hello world, to stdout + * console.log('hello %s', 'world'); + * // Prints: hello world, to stdout + * console.error(new Error('Whoops, something bad happened')); + * // Prints error message and stack trace to stderr: + * // Error: Whoops, something bad happened + * // at [eval]:5:15 + * // at Script.runInThisContext (node:vm:132:18) + * // at Object.runInThisContext (node:vm:309:38) + * // at node:internal/process/execution:77:19 + * // at [eval]-wrapper:6:22 + * // at evalScript (node:internal/process/execution:76:60) + * // at node:internal/main/eval_string:23:3 + * + * const name = 'Will Robinson'; + * console.warn(`Danger ${name}! Danger!`); + * // Prints: Danger Will Robinson! Danger!, to stderr + * ``` + * + * Example using the `Console` class: + * + * ```js + * const out = getStreamSomehow(); + * const err = getStreamSomehow(); + * const myConsole = new console.Console(out, err); + * + * myConsole.log('hello world'); + * // Prints: hello world, to out + * myConsole.log('hello %s', 'world'); + * // Prints: hello world, to out + * myConsole.error(new Error('Whoops, something bad happened')); + * // Prints: [Error: Whoops, something bad happened], to err + * + * const name = 'Will Robinson'; + * myConsole.warn(`Danger ${name}! Danger!`); + * // Prints: Danger Will Robinson! Danger!, to err + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/console.js) + */ + namespace console { + interface ConsoleConstructorOptions { + stdout: NodeJS.WritableStream; + stderr?: NodeJS.WritableStream | undefined; + /** + * Ignore errors when writing to the underlying streams. + * @default true + */ + ignoreErrors?: boolean | undefined; + /** + * Set color support for this `Console` instance. Setting to true enables coloring while inspecting + * values. Setting to `false` disables coloring while inspecting values. Setting to `'auto'` makes color + * support depend on the value of the `isTTY` property and the value returned by `getColorDepth()` on the + * respective stream. This option can not be used, if `inspectOptions.colors` is set as well. + * @default auto + */ + colorMode?: boolean | "auto" | undefined; + /** + * Specifies options that are passed along to + * [`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options). + */ + inspectOptions?: InspectOptions | undefined; + /** + * Set group indentation. + * @default 2 + */ + groupIndentation?: number | undefined; + } + interface ConsoleConstructor { + prototype: Console; + new(stdout: NodeJS.WritableStream, stderr?: NodeJS.WritableStream, ignoreErrors?: boolean): Console; + new(options: ConsoleConstructorOptions): Console; + } + } + var console: Console; + } + export = globalThis.console; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/constants.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/constants.d.ts new file mode 100644 index 00000000..5685a9df --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/constants.d.ts @@ -0,0 +1,21 @@ +/** + * @deprecated The `node:constants` module is deprecated. When requiring access to constants + * relevant to specific Node.js builtin modules, developers should instead refer + * to the `constants` property exposed by the relevant module. For instance, + * `require('node:fs').constants` and `require('node:os').constants`. + */ +declare module "constants" { + const constants: + & typeof import("node:os").constants.dlopen + & typeof import("node:os").constants.errno + & typeof import("node:os").constants.priority + & typeof import("node:os").constants.signals + & typeof import("node:fs").constants + & typeof import("node:crypto").constants; + export = constants; +} + +declare module "node:constants" { + import constants = require("constants"); + export = constants; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/crypto.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/crypto.d.ts new file mode 100644 index 00000000..90238053 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/crypto.d.ts @@ -0,0 +1,4545 @@ +/** + * The `node:crypto` module provides cryptographic functionality that includes a + * set of wrappers for OpenSSL's hash, HMAC, cipher, decipher, sign, and verify + * functions. + * + * ```js + * const { createHmac } = await import('node:crypto'); + * + * const secret = 'abcdefg'; + * const hash = createHmac('sha256', secret) + * .update('I love cupcakes') + * .digest('hex'); + * console.log(hash); + * // Prints: + * // c0fa1bc00531bd78ef38c628449c5102aeabd49b5dc3a2a516ea6ea959d6658e + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/crypto.js) + */ +declare module "crypto" { + import { NonSharedBuffer } from "node:buffer"; + import * as stream from "node:stream"; + import { PeerCertificate } from "node:tls"; + /** + * SPKAC is a Certificate Signing Request mechanism originally implemented by + * Netscape and was specified formally as part of HTML5's `keygen` element. + * + * `` is deprecated since [HTML 5.2](https://www.w3.org/TR/html52/changes.html#features-removed) and new projects + * should not use this element anymore. + * + * The `node:crypto` module provides the `Certificate` class for working with SPKAC + * data. The most common usage is handling output generated by the HTML5 `` element. Node.js uses [OpenSSL's SPKAC + * implementation](https://www.openssl.org/docs/man3.0/man1/openssl-spkac.html) internally. + * @since v0.11.8 + */ + class Certificate { + /** + * ```js + * const { Certificate } = await import('node:crypto'); + * const spkac = getSpkacSomehow(); + * const challenge = Certificate.exportChallenge(spkac); + * console.log(challenge.toString('utf8')); + * // Prints: the challenge as a UTF8 string + * ``` + * @since v9.0.0 + * @param encoding The `encoding` of the `spkac` string. + * @return The challenge component of the `spkac` data structure, which includes a public key and a challenge. + */ + static exportChallenge(spkac: BinaryLike): NonSharedBuffer; + /** + * ```js + * const { Certificate } = await import('node:crypto'); + * const spkac = getSpkacSomehow(); + * const publicKey = Certificate.exportPublicKey(spkac); + * console.log(publicKey); + * // Prints: the public key as + * ``` + * @since v9.0.0 + * @param encoding The `encoding` of the `spkac` string. + * @return The public key component of the `spkac` data structure, which includes a public key and a challenge. + */ + static exportPublicKey(spkac: BinaryLike, encoding?: string): NonSharedBuffer; + /** + * ```js + * import { Buffer } from 'node:buffer'; + * const { Certificate } = await import('node:crypto'); + * + * const spkac = getSpkacSomehow(); + * console.log(Certificate.verifySpkac(Buffer.from(spkac))); + * // Prints: true or false + * ``` + * @since v9.0.0 + * @param encoding The `encoding` of the `spkac` string. + * @return `true` if the given `spkac` data structure is valid, `false` otherwise. + */ + static verifySpkac(spkac: NodeJS.ArrayBufferView): boolean; + /** + * @deprecated + * @param spkac + * @returns The challenge component of the `spkac` data structure, + * which includes a public key and a challenge. + */ + exportChallenge(spkac: BinaryLike): NonSharedBuffer; + /** + * @deprecated + * @param spkac + * @param encoding The encoding of the spkac string. + * @returns The public key component of the `spkac` data structure, + * which includes a public key and a challenge. + */ + exportPublicKey(spkac: BinaryLike, encoding?: string): NonSharedBuffer; + /** + * @deprecated + * @param spkac + * @returns `true` if the given `spkac` data structure is valid, + * `false` otherwise. + */ + verifySpkac(spkac: NodeJS.ArrayBufferView): boolean; + } + namespace constants { + // https://nodejs.org/dist/latest-v22.x/docs/api/crypto.html#crypto-constants + const OPENSSL_VERSION_NUMBER: number; + /** Applies multiple bug workarounds within OpenSSL. See https://www.openssl.org/docs/man1.0.2/ssl/SSL_CTX_set_options.html for detail. */ + const SSL_OP_ALL: number; + /** Instructs OpenSSL to allow a non-[EC]DHE-based key exchange mode for TLS v1.3 */ + const SSL_OP_ALLOW_NO_DHE_KEX: number; + /** Allows legacy insecure renegotiation between OpenSSL and unpatched clients or servers. See https://www.openssl.org/docs/man1.0.2/ssl/SSL_CTX_set_options.html. */ + const SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION: number; + /** Attempts to use the server's preferences instead of the client's when selecting a cipher. See https://www.openssl.org/docs/man1.0.2/ssl/SSL_CTX_set_options.html. */ + const SSL_OP_CIPHER_SERVER_PREFERENCE: number; + /** Instructs OpenSSL to use Cisco's version identifier of DTLS_BAD_VER. */ + const SSL_OP_CISCO_ANYCONNECT: number; + /** Instructs OpenSSL to turn on cookie exchange. */ + const SSL_OP_COOKIE_EXCHANGE: number; + /** Instructs OpenSSL to add server-hello extension from an early version of the cryptopro draft. */ + const SSL_OP_CRYPTOPRO_TLSEXT_BUG: number; + /** Instructs OpenSSL to disable a SSL 3.0/TLS 1.0 vulnerability workaround added in OpenSSL 0.9.6d. */ + const SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS: number; + /** Allows initial connection to servers that do not support RI. */ + const SSL_OP_LEGACY_SERVER_CONNECT: number; + /** Instructs OpenSSL to disable support for SSL/TLS compression. */ + const SSL_OP_NO_COMPRESSION: number; + /** Instructs OpenSSL to disable encrypt-then-MAC. */ + const SSL_OP_NO_ENCRYPT_THEN_MAC: number; + const SSL_OP_NO_QUERY_MTU: number; + /** Instructs OpenSSL to disable renegotiation. */ + const SSL_OP_NO_RENEGOTIATION: number; + /** Instructs OpenSSL to always start a new session when performing renegotiation. */ + const SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION: number; + /** Instructs OpenSSL to turn off SSL v2 */ + const SSL_OP_NO_SSLv2: number; + /** Instructs OpenSSL to turn off SSL v3 */ + const SSL_OP_NO_SSLv3: number; + /** Instructs OpenSSL to disable use of RFC4507bis tickets. */ + const SSL_OP_NO_TICKET: number; + /** Instructs OpenSSL to turn off TLS v1 */ + const SSL_OP_NO_TLSv1: number; + /** Instructs OpenSSL to turn off TLS v1.1 */ + const SSL_OP_NO_TLSv1_1: number; + /** Instructs OpenSSL to turn off TLS v1.2 */ + const SSL_OP_NO_TLSv1_2: number; + /** Instructs OpenSSL to turn off TLS v1.3 */ + const SSL_OP_NO_TLSv1_3: number; + /** Instructs OpenSSL server to prioritize ChaCha20-Poly1305 when the client does. This option has no effect if `SSL_OP_CIPHER_SERVER_PREFERENCE` is not enabled. */ + const SSL_OP_PRIORITIZE_CHACHA: number; + /** Instructs OpenSSL to disable version rollback attack detection. */ + const SSL_OP_TLS_ROLLBACK_BUG: number; + const ENGINE_METHOD_RSA: number; + const ENGINE_METHOD_DSA: number; + const ENGINE_METHOD_DH: number; + const ENGINE_METHOD_RAND: number; + const ENGINE_METHOD_EC: number; + const ENGINE_METHOD_CIPHERS: number; + const ENGINE_METHOD_DIGESTS: number; + const ENGINE_METHOD_PKEY_METHS: number; + const ENGINE_METHOD_PKEY_ASN1_METHS: number; + const ENGINE_METHOD_ALL: number; + const ENGINE_METHOD_NONE: number; + const DH_CHECK_P_NOT_SAFE_PRIME: number; + const DH_CHECK_P_NOT_PRIME: number; + const DH_UNABLE_TO_CHECK_GENERATOR: number; + const DH_NOT_SUITABLE_GENERATOR: number; + const RSA_PKCS1_PADDING: number; + const RSA_SSLV23_PADDING: number; + const RSA_NO_PADDING: number; + const RSA_PKCS1_OAEP_PADDING: number; + const RSA_X931_PADDING: number; + const RSA_PKCS1_PSS_PADDING: number; + /** Sets the salt length for RSA_PKCS1_PSS_PADDING to the digest size when signing or verifying. */ + const RSA_PSS_SALTLEN_DIGEST: number; + /** Sets the salt length for RSA_PKCS1_PSS_PADDING to the maximum permissible value when signing data. */ + const RSA_PSS_SALTLEN_MAX_SIGN: number; + /** Causes the salt length for RSA_PKCS1_PSS_PADDING to be determined automatically when verifying a signature. */ + const RSA_PSS_SALTLEN_AUTO: number; + const POINT_CONVERSION_COMPRESSED: number; + const POINT_CONVERSION_UNCOMPRESSED: number; + const POINT_CONVERSION_HYBRID: number; + /** Specifies the built-in default cipher list used by Node.js (colon-separated values). */ + const defaultCoreCipherList: string; + /** Specifies the active default cipher list used by the current Node.js process (colon-separated values). */ + const defaultCipherList: string; + } + interface HashOptions extends stream.TransformOptions { + /** + * For XOF hash functions such as `shake256`, the + * outputLength option can be used to specify the desired output length in bytes. + */ + outputLength?: number | undefined; + } + /** @deprecated since v10.0.0 */ + const fips: boolean; + /** + * Creates and returns a `Hash` object that can be used to generate hash digests + * using the given `algorithm`. Optional `options` argument controls stream + * behavior. For XOF hash functions such as `'shake256'`, the `outputLength` option + * can be used to specify the desired output length in bytes. + * + * The `algorithm` is dependent on the available algorithms supported by the + * version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. + * On recent releases of OpenSSL, `openssl list -digest-algorithms` will + * display the available digest algorithms. + * + * Example: generating the sha256 sum of a file + * + * ```js + * import { + * createReadStream, + * } from 'node:fs'; + * import { argv } from 'node:process'; + * const { + * createHash, + * } = await import('node:crypto'); + * + * const filename = argv[2]; + * + * const hash = createHash('sha256'); + * + * const input = createReadStream(filename); + * input.on('readable', () => { + * // Only one element is going to be produced by the + * // hash stream. + * const data = input.read(); + * if (data) + * hash.update(data); + * else { + * console.log(`${hash.digest('hex')} ${filename}`); + * } + * }); + * ``` + * @since v0.1.92 + * @param options `stream.transform` options + */ + function createHash(algorithm: string, options?: HashOptions): Hash; + /** + * Creates and returns an `Hmac` object that uses the given `algorithm` and `key`. + * Optional `options` argument controls stream behavior. + * + * The `algorithm` is dependent on the available algorithms supported by the + * version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. + * On recent releases of OpenSSL, `openssl list -digest-algorithms` will + * display the available digest algorithms. + * + * The `key` is the HMAC key used to generate the cryptographic HMAC hash. If it is + * a `KeyObject`, its type must be `secret`. If it is a string, please consider `caveats when using strings as inputs to cryptographic APIs`. If it was + * obtained from a cryptographically secure source of entropy, such as {@link randomBytes} or {@link generateKey}, its length should not + * exceed the block size of `algorithm` (e.g., 512 bits for SHA-256). + * + * Example: generating the sha256 HMAC of a file + * + * ```js + * import { + * createReadStream, + * } from 'node:fs'; + * import { argv } from 'node:process'; + * const { + * createHmac, + * } = await import('node:crypto'); + * + * const filename = argv[2]; + * + * const hmac = createHmac('sha256', 'a secret'); + * + * const input = createReadStream(filename); + * input.on('readable', () => { + * // Only one element is going to be produced by the + * // hash stream. + * const data = input.read(); + * if (data) + * hmac.update(data); + * else { + * console.log(`${hmac.digest('hex')} ${filename}`); + * } + * }); + * ``` + * @since v0.1.94 + * @param options `stream.transform` options + */ + function createHmac(algorithm: string, key: BinaryLike | KeyObject, options?: stream.TransformOptions): Hmac; + // https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings + type BinaryToTextEncoding = "base64" | "base64url" | "hex" | "binary"; + type CharacterEncoding = "utf8" | "utf-8" | "utf16le" | "utf-16le" | "latin1"; + type LegacyCharacterEncoding = "ascii" | "binary" | "ucs2" | "ucs-2"; + type Encoding = BinaryToTextEncoding | CharacterEncoding | LegacyCharacterEncoding; + type ECDHKeyFormat = "compressed" | "uncompressed" | "hybrid"; + /** + * The `Hash` class is a utility for creating hash digests of data. It can be + * used in one of two ways: + * + * * As a `stream` that is both readable and writable, where data is written + * to produce a computed hash digest on the readable side, or + * * Using the `hash.update()` and `hash.digest()` methods to produce the + * computed hash. + * + * The {@link createHash} method is used to create `Hash` instances. `Hash`objects are not to be created directly using the `new` keyword. + * + * Example: Using `Hash` objects as streams: + * + * ```js + * const { + * createHash, + * } = await import('node:crypto'); + * + * const hash = createHash('sha256'); + * + * hash.on('readable', () => { + * // Only one element is going to be produced by the + * // hash stream. + * const data = hash.read(); + * if (data) { + * console.log(data.toString('hex')); + * // Prints: + * // 6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50 + * } + * }); + * + * hash.write('some data to hash'); + * hash.end(); + * ``` + * + * Example: Using `Hash` and piped streams: + * + * ```js + * import { createReadStream } from 'node:fs'; + * import { stdout } from 'node:process'; + * const { createHash } = await import('node:crypto'); + * + * const hash = createHash('sha256'); + * + * const input = createReadStream('test.js'); + * input.pipe(hash).setEncoding('hex').pipe(stdout); + * ``` + * + * Example: Using the `hash.update()` and `hash.digest()` methods: + * + * ```js + * const { + * createHash, + * } = await import('node:crypto'); + * + * const hash = createHash('sha256'); + * + * hash.update('some data to hash'); + * console.log(hash.digest('hex')); + * // Prints: + * // 6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50 + * ``` + * @since v0.1.92 + */ + class Hash extends stream.Transform { + private constructor(); + /** + * Creates a new `Hash` object that contains a deep copy of the internal state + * of the current `Hash` object. + * + * The optional `options` argument controls stream behavior. For XOF hash + * functions such as `'shake256'`, the `outputLength` option can be used to + * specify the desired output length in bytes. + * + * An error is thrown when an attempt is made to copy the `Hash` object after + * its `hash.digest()` method has been called. + * + * ```js + * // Calculate a rolling hash. + * const { + * createHash, + * } = await import('node:crypto'); + * + * const hash = createHash('sha256'); + * + * hash.update('one'); + * console.log(hash.copy().digest('hex')); + * + * hash.update('two'); + * console.log(hash.copy().digest('hex')); + * + * hash.update('three'); + * console.log(hash.copy().digest('hex')); + * + * // Etc. + * ``` + * @since v13.1.0 + * @param options `stream.transform` options + */ + copy(options?: HashOptions): Hash; + /** + * Updates the hash content with the given `data`, the encoding of which + * is given in `inputEncoding`. + * If `encoding` is not provided, and the `data` is a string, an + * encoding of `'utf8'` is enforced. If `data` is a `Buffer`, `TypedArray`, or`DataView`, then `inputEncoding` is ignored. + * + * This can be called many times with new data as it is streamed. + * @since v0.1.92 + * @param inputEncoding The `encoding` of the `data` string. + */ + update(data: BinaryLike): Hash; + update(data: string, inputEncoding: Encoding): Hash; + /** + * Calculates the digest of all of the data passed to be hashed (using the `hash.update()` method). + * If `encoding` is provided a string will be returned; otherwise + * a `Buffer` is returned. + * + * The `Hash` object can not be used again after `hash.digest()` method has been + * called. Multiple calls will cause an error to be thrown. + * @since v0.1.92 + * @param encoding The `encoding` of the return value. + */ + digest(): NonSharedBuffer; + digest(encoding: BinaryToTextEncoding): string; + } + /** + * The `Hmac` class is a utility for creating cryptographic HMAC digests. It can + * be used in one of two ways: + * + * * As a `stream` that is both readable and writable, where data is written + * to produce a computed HMAC digest on the readable side, or + * * Using the `hmac.update()` and `hmac.digest()` methods to produce the + * computed HMAC digest. + * + * The {@link createHmac} method is used to create `Hmac` instances. `Hmac`objects are not to be created directly using the `new` keyword. + * + * Example: Using `Hmac` objects as streams: + * + * ```js + * const { + * createHmac, + * } = await import('node:crypto'); + * + * const hmac = createHmac('sha256', 'a secret'); + * + * hmac.on('readable', () => { + * // Only one element is going to be produced by the + * // hash stream. + * const data = hmac.read(); + * if (data) { + * console.log(data.toString('hex')); + * // Prints: + * // 7fd04df92f636fd450bc841c9418e5825c17f33ad9c87c518115a45971f7f77e + * } + * }); + * + * hmac.write('some data to hash'); + * hmac.end(); + * ``` + * + * Example: Using `Hmac` and piped streams: + * + * ```js + * import { createReadStream } from 'node:fs'; + * import { stdout } from 'node:process'; + * const { + * createHmac, + * } = await import('node:crypto'); + * + * const hmac = createHmac('sha256', 'a secret'); + * + * const input = createReadStream('test.js'); + * input.pipe(hmac).pipe(stdout); + * ``` + * + * Example: Using the `hmac.update()` and `hmac.digest()` methods: + * + * ```js + * const { + * createHmac, + * } = await import('node:crypto'); + * + * const hmac = createHmac('sha256', 'a secret'); + * + * hmac.update('some data to hash'); + * console.log(hmac.digest('hex')); + * // Prints: + * // 7fd04df92f636fd450bc841c9418e5825c17f33ad9c87c518115a45971f7f77e + * ``` + * @since v0.1.94 + * @deprecated Since v20.13.0 Calling `Hmac` class directly with `Hmac()` or `new Hmac()` is deprecated due to being internals, not intended for public use. Please use the {@link createHmac} method to create Hmac instances. + */ + class Hmac extends stream.Transform { + private constructor(); + /** + * Updates the `Hmac` content with the given `data`, the encoding of which + * is given in `inputEncoding`. + * If `encoding` is not provided, and the `data` is a string, an + * encoding of `'utf8'` is enforced. If `data` is a `Buffer`, `TypedArray`, or`DataView`, then `inputEncoding` is ignored. + * + * This can be called many times with new data as it is streamed. + * @since v0.1.94 + * @param inputEncoding The `encoding` of the `data` string. + */ + update(data: BinaryLike): Hmac; + update(data: string, inputEncoding: Encoding): Hmac; + /** + * Calculates the HMAC digest of all of the data passed using `hmac.update()`. + * If `encoding` is + * provided a string is returned; otherwise a `Buffer` is returned; + * + * The `Hmac` object can not be used again after `hmac.digest()` has been + * called. Multiple calls to `hmac.digest()` will result in an error being thrown. + * @since v0.1.94 + * @param encoding The `encoding` of the return value. + */ + digest(): NonSharedBuffer; + digest(encoding: BinaryToTextEncoding): string; + } + type KeyObjectType = "secret" | "public" | "private"; + interface KeyExportOptions { + type: "pkcs1" | "spki" | "pkcs8" | "sec1"; + format: T; + cipher?: string | undefined; + passphrase?: string | Buffer | undefined; + } + interface JwkKeyExportOptions { + format: "jwk"; + } + interface JsonWebKey { + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + k?: string; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + x?: string; + y?: string; + [key: string]: unknown; + } + interface AsymmetricKeyDetails { + /** + * Key size in bits (RSA, DSA). + */ + modulusLength?: number; + /** + * Public exponent (RSA). + */ + publicExponent?: bigint; + /** + * Name of the message digest (RSA-PSS). + */ + hashAlgorithm?: string; + /** + * Name of the message digest used by MGF1 (RSA-PSS). + */ + mgf1HashAlgorithm?: string; + /** + * Minimal salt length in bytes (RSA-PSS). + */ + saltLength?: number; + /** + * Size of q in bits (DSA). + */ + divisorLength?: number; + /** + * Name of the curve (EC). + */ + namedCurve?: string; + } + /** + * Node.js uses a `KeyObject` class to represent a symmetric or asymmetric key, + * and each kind of key exposes different functions. The {@link createSecretKey}, {@link createPublicKey} and {@link createPrivateKey} methods are used to create `KeyObject`instances. `KeyObject` + * objects are not to be created directly using the `new`keyword. + * + * Most applications should consider using the new `KeyObject` API instead of + * passing keys as strings or `Buffer`s due to improved security features. + * + * `KeyObject` instances can be passed to other threads via `postMessage()`. + * The receiver obtains a cloned `KeyObject`, and the `KeyObject` does not need to + * be listed in the `transferList` argument. + * @since v11.6.0 + */ + class KeyObject { + private constructor(); + /** + * Example: Converting a `CryptoKey` instance to a `KeyObject`: + * + * ```js + * const { KeyObject } = await import('node:crypto'); + * const { subtle } = globalThis.crypto; + * + * const key = await subtle.generateKey({ + * name: 'HMAC', + * hash: 'SHA-256', + * length: 256, + * }, true, ['sign', 'verify']); + * + * const keyObject = KeyObject.from(key); + * console.log(keyObject.symmetricKeySize); + * // Prints: 32 (symmetric key size in bytes) + * ``` + * @since v15.0.0 + */ + static from(key: webcrypto.CryptoKey): KeyObject; + /** + * For asymmetric keys, this property represents the type of the key. Supported key + * types are: + * + * * `'rsa'` (OID 1.2.840.113549.1.1.1) + * * `'rsa-pss'` (OID 1.2.840.113549.1.1.10) + * * `'dsa'` (OID 1.2.840.10040.4.1) + * * `'ec'` (OID 1.2.840.10045.2.1) + * * `'x25519'` (OID 1.3.101.110) + * * `'x448'` (OID 1.3.101.111) + * * `'ed25519'` (OID 1.3.101.112) + * * `'ed448'` (OID 1.3.101.113) + * * `'dh'` (OID 1.2.840.113549.1.3.1) + * + * This property is `undefined` for unrecognized `KeyObject` types and symmetric + * keys. + * @since v11.6.0 + */ + asymmetricKeyType?: KeyType; + /** + * This property exists only on asymmetric keys. Depending on the type of the key, + * this object contains information about the key. None of the information obtained + * through this property can be used to uniquely identify a key or to compromise + * the security of the key. + * + * For RSA-PSS keys, if the key material contains a `RSASSA-PSS-params` sequence, + * the `hashAlgorithm`, `mgf1HashAlgorithm`, and `saltLength` properties will be + * set. + * + * Other key details might be exposed via this API using additional attributes. + * @since v15.7.0 + */ + asymmetricKeyDetails?: AsymmetricKeyDetails; + /** + * For symmetric keys, the following encoding options can be used: + * + * For public keys, the following encoding options can be used: + * + * For private keys, the following encoding options can be used: + * + * The result type depends on the selected encoding format, when PEM the + * result is a string, when DER it will be a buffer containing the data + * encoded as DER, when [JWK](https://tools.ietf.org/html/rfc7517) it will be an object. + * + * When [JWK](https://tools.ietf.org/html/rfc7517) encoding format was selected, all other encoding options are + * ignored. + * + * PKCS#1, SEC1, and PKCS#8 type keys can be encrypted by using a combination of + * the `cipher` and `format` options. The PKCS#8 `type` can be used with any`format` to encrypt any key algorithm (RSA, EC, or DH) by specifying a`cipher`. PKCS#1 and SEC1 can only be + * encrypted by specifying a `cipher`when the PEM `format` is used. For maximum compatibility, use PKCS#8 for + * encrypted private keys. Since PKCS#8 defines its own + * encryption mechanism, PEM-level encryption is not supported when encrypting + * a PKCS#8 key. See [RFC 5208](https://www.rfc-editor.org/rfc/rfc5208.txt) for PKCS#8 encryption and [RFC 1421](https://www.rfc-editor.org/rfc/rfc1421.txt) for + * PKCS#1 and SEC1 encryption. + * @since v11.6.0 + */ + export(options: KeyExportOptions<"pem">): string | NonSharedBuffer; + export(options?: KeyExportOptions<"der">): NonSharedBuffer; + export(options?: JwkKeyExportOptions): JsonWebKey; + /** + * Returns `true` or `false` depending on whether the keys have exactly the same + * type, value, and parameters. This method is not [constant time](https://en.wikipedia.org/wiki/Timing_attack). + * @since v17.7.0, v16.15.0 + * @param otherKeyObject A `KeyObject` with which to compare `keyObject`. + */ + equals(otherKeyObject: KeyObject): boolean; + /** + * For secret keys, this property represents the size of the key in bytes. This + * property is `undefined` for asymmetric keys. + * @since v11.6.0 + */ + symmetricKeySize?: number; + /** + * Converts a `KeyObject` instance to a `CryptoKey`. + * @since 22.10.0 + */ + toCryptoKey( + algorithm: + | webcrypto.AlgorithmIdentifier + | webcrypto.RsaHashedImportParams + | webcrypto.EcKeyImportParams + | webcrypto.HmacImportParams, + extractable: boolean, + keyUsages: readonly webcrypto.KeyUsage[], + ): webcrypto.CryptoKey; + /** + * Depending on the type of this `KeyObject`, this property is either`'secret'` for secret (symmetric) keys, `'public'` for public (asymmetric) keys + * or `'private'` for private (asymmetric) keys. + * @since v11.6.0 + */ + type: KeyObjectType; + } + type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm"; + type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; + type CipherOCBTypes = "aes-128-ocb" | "aes-192-ocb" | "aes-256-ocb"; + type CipherChaCha20Poly1305Types = "chacha20-poly1305"; + type BinaryLike = string | NodeJS.ArrayBufferView; + type CipherKey = BinaryLike | KeyObject; + interface CipherCCMOptions extends stream.TransformOptions { + authTagLength: number; + } + interface CipherGCMOptions extends stream.TransformOptions { + authTagLength?: number | undefined; + } + interface CipherOCBOptions extends stream.TransformOptions { + authTagLength: number; + } + interface CipherChaCha20Poly1305Options extends stream.TransformOptions { + /** @default 16 */ + authTagLength?: number | undefined; + } + /** + * Creates and returns a `Cipher` object, with the given `algorithm`, `key` and + * initialization vector (`iv`). + * + * The `options` argument controls stream behavior and is optional except when a + * cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the`authTagLength` option is required and specifies the length of the + * authentication tag in bytes, see `CCM mode`. In GCM mode, the `authTagLength`option is not required but can be used to set the length of the authentication + * tag that will be returned by `getAuthTag()` and defaults to 16 bytes. + * For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes. + * + * The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On + * recent OpenSSL releases, `openssl list -cipher-algorithms` will + * display the available cipher algorithms. + * + * The `key` is the raw key used by the `algorithm` and `iv` is an [initialization vector](https://en.wikipedia.org/wiki/Initialization_vector). Both arguments must be `'utf8'` encoded + * strings,`Buffers`, `TypedArray`, or `DataView`s. The `key` may optionally be + * a `KeyObject` of type `secret`. If the cipher does not need + * an initialization vector, `iv` may be `null`. + * + * When passing strings for `key` or `iv`, please consider `caveats when using strings as inputs to cryptographic APIs`. + * + * Initialization vectors should be unpredictable and unique; ideally, they will be + * cryptographically random. They do not have to be secret: IVs are typically just + * added to ciphertext messages unencrypted. It may sound contradictory that + * something has to be unpredictable and unique, but does not have to be secret; + * remember that an attacker must not be able to predict ahead of time what a + * given IV will be. + * @since v0.1.94 + * @param options `stream.transform` options + */ + function createCipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): CipherCCM; + function createCipheriv( + algorithm: CipherOCBTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherOCBOptions, + ): CipherOCB; + function createCipheriv( + algorithm: CipherGCMTypes, + key: CipherKey, + iv: BinaryLike, + options?: CipherGCMOptions, + ): CipherGCM; + function createCipheriv( + algorithm: CipherChaCha20Poly1305Types, + key: CipherKey, + iv: BinaryLike, + options?: CipherChaCha20Poly1305Options, + ): CipherChaCha20Poly1305; + function createCipheriv( + algorithm: string, + key: CipherKey, + iv: BinaryLike | null, + options?: stream.TransformOptions, + ): Cipher; + /** + * Instances of the `Cipher` class are used to encrypt data. The class can be + * used in one of two ways: + * + * * As a `stream` that is both readable and writable, where plain unencrypted + * data is written to produce encrypted data on the readable side, or + * * Using the `cipher.update()` and `cipher.final()` methods to produce + * the encrypted data. + * + * The {@link createCipheriv} method is + * used to create `Cipher` instances. `Cipher` objects are not to be created + * directly using the `new` keyword. + * + * Example: Using `Cipher` objects as streams: + * + * ```js + * const { + * scrypt, + * randomFill, + * createCipheriv, + * } = await import('node:crypto'); + * + * const algorithm = 'aes-192-cbc'; + * const password = 'Password used to generate key'; + * + * // First, we'll generate the key. The key length is dependent on the algorithm. + * // In this case for aes192, it is 24 bytes (192 bits). + * scrypt(password, 'salt', 24, (err, key) => { + * if (err) throw err; + * // Then, we'll generate a random initialization vector + * randomFill(new Uint8Array(16), (err, iv) => { + * if (err) throw err; + * + * // Once we have the key and iv, we can create and use the cipher... + * const cipher = createCipheriv(algorithm, key, iv); + * + * let encrypted = ''; + * cipher.setEncoding('hex'); + * + * cipher.on('data', (chunk) => encrypted += chunk); + * cipher.on('end', () => console.log(encrypted)); + * + * cipher.write('some clear text data'); + * cipher.end(); + * }); + * }); + * ``` + * + * Example: Using `Cipher` and piped streams: + * + * ```js + * import { + * createReadStream, + * createWriteStream, + * } from 'node:fs'; + * + * import { + * pipeline, + * } from 'node:stream'; + * + * const { + * scrypt, + * randomFill, + * createCipheriv, + * } = await import('node:crypto'); + * + * const algorithm = 'aes-192-cbc'; + * const password = 'Password used to generate key'; + * + * // First, we'll generate the key. The key length is dependent on the algorithm. + * // In this case for aes192, it is 24 bytes (192 bits). + * scrypt(password, 'salt', 24, (err, key) => { + * if (err) throw err; + * // Then, we'll generate a random initialization vector + * randomFill(new Uint8Array(16), (err, iv) => { + * if (err) throw err; + * + * const cipher = createCipheriv(algorithm, key, iv); + * + * const input = createReadStream('test.js'); + * const output = createWriteStream('test.enc'); + * + * pipeline(input, cipher, output, (err) => { + * if (err) throw err; + * }); + * }); + * }); + * ``` + * + * Example: Using the `cipher.update()` and `cipher.final()` methods: + * + * ```js + * const { + * scrypt, + * randomFill, + * createCipheriv, + * } = await import('node:crypto'); + * + * const algorithm = 'aes-192-cbc'; + * const password = 'Password used to generate key'; + * + * // First, we'll generate the key. The key length is dependent on the algorithm. + * // In this case for aes192, it is 24 bytes (192 bits). + * scrypt(password, 'salt', 24, (err, key) => { + * if (err) throw err; + * // Then, we'll generate a random initialization vector + * randomFill(new Uint8Array(16), (err, iv) => { + * if (err) throw err; + * + * const cipher = createCipheriv(algorithm, key, iv); + * + * let encrypted = cipher.update('some clear text data', 'utf8', 'hex'); + * encrypted += cipher.final('hex'); + * console.log(encrypted); + * }); + * }); + * ``` + * @since v0.1.94 + */ + class Cipher extends stream.Transform { + private constructor(); + /** + * Updates the cipher with `data`. If the `inputEncoding` argument is given, + * the `data`argument is a string using the specified encoding. If the `inputEncoding`argument is not given, `data` must be a `Buffer`, `TypedArray`, or `DataView`. If `data` is a `Buffer`, + * `TypedArray`, or `DataView`, then `inputEncoding` is ignored. + * + * The `outputEncoding` specifies the output format of the enciphered + * data. If the `outputEncoding`is specified, a string using the specified encoding is returned. If no`outputEncoding` is provided, a `Buffer` is returned. + * + * The `cipher.update()` method can be called multiple times with new data until `cipher.final()` is called. Calling `cipher.update()` after `cipher.final()` will result in an error being + * thrown. + * @since v0.1.94 + * @param inputEncoding The `encoding` of the data. + * @param outputEncoding The `encoding` of the return value. + */ + update(data: BinaryLike): NonSharedBuffer; + update(data: string, inputEncoding: Encoding): NonSharedBuffer; + update(data: NodeJS.ArrayBufferView, inputEncoding: undefined, outputEncoding: Encoding): string; + update(data: string, inputEncoding: Encoding | undefined, outputEncoding: Encoding): string; + /** + * Once the `cipher.final()` method has been called, the `Cipher` object can no + * longer be used to encrypt data. Attempts to call `cipher.final()` more than + * once will result in an error being thrown. + * @since v0.1.94 + * @param outputEncoding The `encoding` of the return value. + * @return Any remaining enciphered contents. If `outputEncoding` is specified, a string is returned. If an `outputEncoding` is not provided, a {@link Buffer} is returned. + */ + final(): NonSharedBuffer; + final(outputEncoding: BufferEncoding): string; + /** + * When using block encryption algorithms, the `Cipher` class will automatically + * add padding to the input data to the appropriate block size. To disable the + * default padding call `cipher.setAutoPadding(false)`. + * + * When `autoPadding` is `false`, the length of the entire input data must be a + * multiple of the cipher's block size or `cipher.final()` will throw an error. + * Disabling automatic padding is useful for non-standard padding, for instance + * using `0x0` instead of PKCS padding. + * + * The `cipher.setAutoPadding()` method must be called before `cipher.final()`. + * @since v0.7.1 + * @param [autoPadding=true] + * @return for method chaining. + */ + setAutoPadding(autoPadding?: boolean): this; + } + interface CipherCCM extends Cipher { + setAAD( + buffer: NodeJS.ArrayBufferView, + options: { + plaintextLength: number; + }, + ): this; + getAuthTag(): NonSharedBuffer; + } + interface CipherGCM extends Cipher { + setAAD( + buffer: NodeJS.ArrayBufferView, + options?: { + plaintextLength: number; + }, + ): this; + getAuthTag(): NonSharedBuffer; + } + interface CipherOCB extends Cipher { + setAAD( + buffer: NodeJS.ArrayBufferView, + options?: { + plaintextLength: number; + }, + ): this; + getAuthTag(): NonSharedBuffer; + } + interface CipherChaCha20Poly1305 extends Cipher { + setAAD( + buffer: NodeJS.ArrayBufferView, + options: { + plaintextLength: number; + }, + ): this; + getAuthTag(): NonSharedBuffer; + } + /** + * Creates and returns a `Decipher` object that uses the given `algorithm`, `key` and initialization vector (`iv`). + * + * The `options` argument controls stream behavior and is optional except when a + * cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the `authTagLength` option is required and specifies the length of the + * authentication tag in bytes, see `CCM mode`. In GCM mode, the `authTagLength` option is not required but can be used to restrict accepted authentication tags + * to those with the specified length. + * For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes. + * + * The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On + * recent OpenSSL releases, `openssl list -cipher-algorithms` will + * display the available cipher algorithms. + * + * The `key` is the raw key used by the `algorithm` and `iv` is an [initialization vector](https://en.wikipedia.org/wiki/Initialization_vector). Both arguments must be `'utf8'` encoded + * strings,`Buffers`, `TypedArray`, or `DataView`s. The `key` may optionally be + * a `KeyObject` of type `secret`. If the cipher does not need + * an initialization vector, `iv` may be `null`. + * + * When passing strings for `key` or `iv`, please consider `caveats when using strings as inputs to cryptographic APIs`. + * + * Initialization vectors should be unpredictable and unique; ideally, they will be + * cryptographically random. They do not have to be secret: IVs are typically just + * added to ciphertext messages unencrypted. It may sound contradictory that + * something has to be unpredictable and unique, but does not have to be secret; + * remember that an attacker must not be able to predict ahead of time what a given + * IV will be. + * @since v0.1.94 + * @param options `stream.transform` options + */ + function createDecipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): DecipherCCM; + function createDecipheriv( + algorithm: CipherOCBTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherOCBOptions, + ): DecipherOCB; + function createDecipheriv( + algorithm: CipherGCMTypes, + key: CipherKey, + iv: BinaryLike, + options?: CipherGCMOptions, + ): DecipherGCM; + function createDecipheriv( + algorithm: CipherChaCha20Poly1305Types, + key: CipherKey, + iv: BinaryLike, + options?: CipherChaCha20Poly1305Options, + ): DecipherChaCha20Poly1305; + function createDecipheriv( + algorithm: string, + key: CipherKey, + iv: BinaryLike | null, + options?: stream.TransformOptions, + ): Decipher; + /** + * Instances of the `Decipher` class are used to decrypt data. The class can be + * used in one of two ways: + * + * * As a `stream` that is both readable and writable, where plain encrypted + * data is written to produce unencrypted data on the readable side, or + * * Using the `decipher.update()` and `decipher.final()` methods to + * produce the unencrypted data. + * + * The {@link createDecipheriv} method is + * used to create `Decipher` instances. `Decipher` objects are not to be created + * directly using the `new` keyword. + * + * Example: Using `Decipher` objects as streams: + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { + * scryptSync, + * createDecipheriv, + * } = await import('node:crypto'); + * + * const algorithm = 'aes-192-cbc'; + * const password = 'Password used to generate key'; + * // Key length is dependent on the algorithm. In this case for aes192, it is + * // 24 bytes (192 bits). + * // Use the async `crypto.scrypt()` instead. + * const key = scryptSync(password, 'salt', 24); + * // The IV is usually passed along with the ciphertext. + * const iv = Buffer.alloc(16, 0); // Initialization vector. + * + * const decipher = createDecipheriv(algorithm, key, iv); + * + * let decrypted = ''; + * decipher.on('readable', () => { + * let chunk; + * while (null !== (chunk = decipher.read())) { + * decrypted += chunk.toString('utf8'); + * } + * }); + * decipher.on('end', () => { + * console.log(decrypted); + * // Prints: some clear text data + * }); + * + * // Encrypted with same algorithm, key and iv. + * const encrypted = + * 'e5f79c5915c02171eec6b212d5520d44480993d7d622a7c4c2da32f6efda0ffa'; + * decipher.write(encrypted, 'hex'); + * decipher.end(); + * ``` + * + * Example: Using `Decipher` and piped streams: + * + * ```js + * import { + * createReadStream, + * createWriteStream, + * } from 'node:fs'; + * import { Buffer } from 'node:buffer'; + * const { + * scryptSync, + * createDecipheriv, + * } = await import('node:crypto'); + * + * const algorithm = 'aes-192-cbc'; + * const password = 'Password used to generate key'; + * // Use the async `crypto.scrypt()` instead. + * const key = scryptSync(password, 'salt', 24); + * // The IV is usually passed along with the ciphertext. + * const iv = Buffer.alloc(16, 0); // Initialization vector. + * + * const decipher = createDecipheriv(algorithm, key, iv); + * + * const input = createReadStream('test.enc'); + * const output = createWriteStream('test.js'); + * + * input.pipe(decipher).pipe(output); + * ``` + * + * Example: Using the `decipher.update()` and `decipher.final()` methods: + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { + * scryptSync, + * createDecipheriv, + * } = await import('node:crypto'); + * + * const algorithm = 'aes-192-cbc'; + * const password = 'Password used to generate key'; + * // Use the async `crypto.scrypt()` instead. + * const key = scryptSync(password, 'salt', 24); + * // The IV is usually passed along with the ciphertext. + * const iv = Buffer.alloc(16, 0); // Initialization vector. + * + * const decipher = createDecipheriv(algorithm, key, iv); + * + * // Encrypted using same algorithm, key and iv. + * const encrypted = + * 'e5f79c5915c02171eec6b212d5520d44480993d7d622a7c4c2da32f6efda0ffa'; + * let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + * decrypted += decipher.final('utf8'); + * console.log(decrypted); + * // Prints: some clear text data + * ``` + * @since v0.1.94 + */ + class Decipher extends stream.Transform { + private constructor(); + /** + * Updates the decipher with `data`. If the `inputEncoding` argument is given, + * the `data` argument is a string using the specified encoding. If the `inputEncoding` argument is not given, `data` must be a `Buffer`. If `data` is a `Buffer` then `inputEncoding` is + * ignored. + * + * The `outputEncoding` specifies the output format of the enciphered + * data. If the `outputEncoding` is specified, a string using the specified encoding is returned. If no `outputEncoding` is provided, a `Buffer` is returned. + * + * The `decipher.update()` method can be called multiple times with new data until `decipher.final()` is called. Calling `decipher.update()` after `decipher.final()` will result in an error + * being thrown. + * @since v0.1.94 + * @param inputEncoding The `encoding` of the `data` string. + * @param outputEncoding The `encoding` of the return value. + */ + update(data: NodeJS.ArrayBufferView): NonSharedBuffer; + update(data: string, inputEncoding: Encoding): NonSharedBuffer; + update(data: NodeJS.ArrayBufferView, inputEncoding: undefined, outputEncoding: Encoding): string; + update(data: string, inputEncoding: Encoding | undefined, outputEncoding: Encoding): string; + /** + * Once the `decipher.final()` method has been called, the `Decipher` object can + * no longer be used to decrypt data. Attempts to call `decipher.final()` more + * than once will result in an error being thrown. + * @since v0.1.94 + * @param outputEncoding The `encoding` of the return value. + * @return Any remaining deciphered contents. If `outputEncoding` is specified, a string is returned. If an `outputEncoding` is not provided, a {@link Buffer} is returned. + */ + final(): NonSharedBuffer; + final(outputEncoding: BufferEncoding): string; + /** + * When data has been encrypted without standard block padding, calling `decipher.setAutoPadding(false)` will disable automatic padding to prevent `decipher.final()` from checking for and + * removing padding. + * + * Turning auto padding off will only work if the input data's length is a + * multiple of the ciphers block size. + * + * The `decipher.setAutoPadding()` method must be called before `decipher.final()`. + * @since v0.7.1 + * @param [autoPadding=true] + * @return for method chaining. + */ + setAutoPadding(auto_padding?: boolean): this; + } + interface DecipherCCM extends Decipher { + setAuthTag(buffer: NodeJS.ArrayBufferView): this; + setAAD( + buffer: NodeJS.ArrayBufferView, + options: { + plaintextLength: number; + }, + ): this; + } + interface DecipherGCM extends Decipher { + setAuthTag(buffer: NodeJS.ArrayBufferView): this; + setAAD( + buffer: NodeJS.ArrayBufferView, + options?: { + plaintextLength: number; + }, + ): this; + } + interface DecipherOCB extends Decipher { + setAuthTag(buffer: NodeJS.ArrayBufferView): this; + setAAD( + buffer: NodeJS.ArrayBufferView, + options?: { + plaintextLength: number; + }, + ): this; + } + interface DecipherChaCha20Poly1305 extends Decipher { + setAuthTag(buffer: NodeJS.ArrayBufferView): this; + setAAD( + buffer: NodeJS.ArrayBufferView, + options: { + plaintextLength: number; + }, + ): this; + } + interface PrivateKeyInput { + key: string | Buffer; + format?: KeyFormat | undefined; + type?: "pkcs1" | "pkcs8" | "sec1" | undefined; + passphrase?: string | Buffer | undefined; + encoding?: string | undefined; + } + interface PublicKeyInput { + key: string | Buffer; + format?: KeyFormat | undefined; + type?: "pkcs1" | "spki" | undefined; + encoding?: string | undefined; + } + /** + * Asynchronously generates a new random secret key of the given `length`. The `type` will determine which validations will be performed on the `length`. + * + * ```js + * const { + * generateKey, + * } = await import('node:crypto'); + * + * generateKey('hmac', { length: 512 }, (err, key) => { + * if (err) throw err; + * console.log(key.export().toString('hex')); // 46e..........620 + * }); + * ``` + * + * The size of a generated HMAC key should not exceed the block size of the + * underlying hash function. See {@link createHmac} for more information. + * @since v15.0.0 + * @param type The intended use of the generated secret key. Currently accepted values are `'hmac'` and `'aes'`. + */ + function generateKey( + type: "hmac" | "aes", + options: { + length: number; + }, + callback: (err: Error | null, key: KeyObject) => void, + ): void; + /** + * Synchronously generates a new random secret key of the given `length`. The `type` will determine which validations will be performed on the `length`. + * + * ```js + * const { + * generateKeySync, + * } = await import('node:crypto'); + * + * const key = generateKeySync('hmac', { length: 512 }); + * console.log(key.export().toString('hex')); // e89..........41e + * ``` + * + * The size of a generated HMAC key should not exceed the block size of the + * underlying hash function. See {@link createHmac} for more information. + * @since v15.0.0 + * @param type The intended use of the generated secret key. Currently accepted values are `'hmac'` and `'aes'`. + */ + function generateKeySync( + type: "hmac" | "aes", + options: { + length: number; + }, + ): KeyObject; + interface JsonWebKeyInput { + key: JsonWebKey; + format: "jwk"; + } + /** + * Creates and returns a new key object containing a private key. If `key` is a + * string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key` must be an object with the properties described above. + * + * If the private key is encrypted, a `passphrase` must be specified. The length + * of the passphrase is limited to 1024 bytes. + * @since v11.6.0 + */ + function createPrivateKey(key: PrivateKeyInput | string | Buffer | JsonWebKeyInput): KeyObject; + /** + * Creates and returns a new key object containing a public key. If `key` is a + * string or `Buffer`, `format` is assumed to be `'pem'`; if `key` is a `KeyObject` with type `'private'`, the public key is derived from the given private key; + * otherwise, `key` must be an object with the properties described above. + * + * If the format is `'pem'`, the `'key'` may also be an X.509 certificate. + * + * Because public keys can be derived from private keys, a private key may be + * passed instead of a public key. In that case, this function behaves as if {@link createPrivateKey} had been called, except that the type of the + * returned `KeyObject` will be `'public'` and that the private key cannot be + * extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type `'private'` is given, a new `KeyObject` with type `'public'` will be returned + * and it will be impossible to extract the private key from the returned object. + * @since v11.6.0 + */ + function createPublicKey(key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput): KeyObject; + /** + * Creates and returns a new key object containing a secret key for symmetric + * encryption or `Hmac`. + * @since v11.6.0 + * @param encoding The string encoding when `key` is a string. + */ + function createSecretKey(key: NodeJS.ArrayBufferView): KeyObject; + function createSecretKey(key: string, encoding: BufferEncoding): KeyObject; + /** + * Creates and returns a `Sign` object that uses the given `algorithm`. Use {@link getHashes} to obtain the names of the available digest algorithms. + * Optional `options` argument controls the `stream.Writable` behavior. + * + * In some cases, a `Sign` instance can be created using the name of a signature + * algorithm, such as `'RSA-SHA256'`, instead of a digest algorithm. This will use + * the corresponding digest algorithm. This does not work for all signature + * algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest + * algorithm names. + * @since v0.1.92 + * @param options `stream.Writable` options + */ + function createSign(algorithm: string, options?: stream.WritableOptions): Sign; + type DSAEncoding = "der" | "ieee-p1363"; + interface SigningOptions { + /** + * @see crypto.constants.RSA_PKCS1_PADDING + */ + padding?: number | undefined; + saltLength?: number | undefined; + dsaEncoding?: DSAEncoding | undefined; + } + interface SignPrivateKeyInput extends PrivateKeyInput, SigningOptions {} + interface SignKeyObjectInput extends SigningOptions { + key: KeyObject; + } + interface SignJsonWebKeyInput extends JsonWebKeyInput, SigningOptions {} + interface VerifyPublicKeyInput extends PublicKeyInput, SigningOptions {} + interface VerifyKeyObjectInput extends SigningOptions { + key: KeyObject; + } + interface VerifyJsonWebKeyInput extends JsonWebKeyInput, SigningOptions {} + type KeyLike = string | Buffer | KeyObject; + /** + * The `Sign` class is a utility for generating signatures. It can be used in one + * of two ways: + * + * * As a writable `stream`, where data to be signed is written and the `sign.sign()` method is used to generate and return the signature, or + * * Using the `sign.update()` and `sign.sign()` methods to produce the + * signature. + * + * The {@link createSign} method is used to create `Sign` instances. The + * argument is the string name of the hash function to use. `Sign` objects are not + * to be created directly using the `new` keyword. + * + * Example: Using `Sign` and `Verify` objects as streams: + * + * ```js + * const { + * generateKeyPairSync, + * createSign, + * createVerify, + * } = await import('node:crypto'); + * + * const { privateKey, publicKey } = generateKeyPairSync('ec', { + * namedCurve: 'sect239k1', + * }); + * + * const sign = createSign('SHA256'); + * sign.write('some data to sign'); + * sign.end(); + * const signature = sign.sign(privateKey, 'hex'); + * + * const verify = createVerify('SHA256'); + * verify.write('some data to sign'); + * verify.end(); + * console.log(verify.verify(publicKey, signature, 'hex')); + * // Prints: true + * ``` + * + * Example: Using the `sign.update()` and `verify.update()` methods: + * + * ```js + * const { + * generateKeyPairSync, + * createSign, + * createVerify, + * } = await import('node:crypto'); + * + * const { privateKey, publicKey } = generateKeyPairSync('rsa', { + * modulusLength: 2048, + * }); + * + * const sign = createSign('SHA256'); + * sign.update('some data to sign'); + * sign.end(); + * const signature = sign.sign(privateKey); + * + * const verify = createVerify('SHA256'); + * verify.update('some data to sign'); + * verify.end(); + * console.log(verify.verify(publicKey, signature)); + * // Prints: true + * ``` + * @since v0.1.92 + */ + class Sign extends stream.Writable { + private constructor(); + /** + * Updates the `Sign` content with the given `data`, the encoding of which + * is given in `inputEncoding`. + * If `encoding` is not provided, and the `data` is a string, an + * encoding of `'utf8'` is enforced. If `data` is a `Buffer`, `TypedArray`, or`DataView`, then `inputEncoding` is ignored. + * + * This can be called many times with new data as it is streamed. + * @since v0.1.92 + * @param inputEncoding The `encoding` of the `data` string. + */ + update(data: BinaryLike): this; + update(data: string, inputEncoding: Encoding): this; + /** + * Calculates the signature on all the data passed through using either `sign.update()` or `sign.write()`. + * + * If `privateKey` is not a `KeyObject`, this function behaves as if `privateKey` had been passed to {@link createPrivateKey}. If it is an + * object, the following additional properties can be passed: + * + * If `outputEncoding` is provided a string is returned; otherwise a `Buffer` is returned. + * + * The `Sign` object can not be again used after `sign.sign()` method has been + * called. Multiple calls to `sign.sign()` will result in an error being thrown. + * @since v0.1.92 + */ + sign(privateKey: KeyLike | SignKeyObjectInput | SignPrivateKeyInput | SignJsonWebKeyInput): NonSharedBuffer; + sign( + privateKey: KeyLike | SignKeyObjectInput | SignPrivateKeyInput | SignJsonWebKeyInput, + outputFormat: BinaryToTextEncoding, + ): string; + } + /** + * Creates and returns a `Verify` object that uses the given algorithm. + * Use {@link getHashes} to obtain an array of names of the available + * signing algorithms. Optional `options` argument controls the `stream.Writable` behavior. + * + * In some cases, a `Verify` instance can be created using the name of a signature + * algorithm, such as `'RSA-SHA256'`, instead of a digest algorithm. This will use + * the corresponding digest algorithm. This does not work for all signature + * algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest + * algorithm names. + * @since v0.1.92 + * @param options `stream.Writable` options + */ + function createVerify(algorithm: string, options?: stream.WritableOptions): Verify; + /** + * The `Verify` class is a utility for verifying signatures. It can be used in one + * of two ways: + * + * * As a writable `stream` where written data is used to validate against the + * supplied signature, or + * * Using the `verify.update()` and `verify.verify()` methods to verify + * the signature. + * + * The {@link createVerify} method is used to create `Verify` instances. `Verify` objects are not to be created directly using the `new` keyword. + * + * See `Sign` for examples. + * @since v0.1.92 + */ + class Verify extends stream.Writable { + private constructor(); + /** + * Updates the `Verify` content with the given `data`, the encoding of which + * is given in `inputEncoding`. + * If `inputEncoding` is not provided, and the `data` is a string, an + * encoding of `'utf8'` is enforced. If `data` is a `Buffer`, `TypedArray`, or `DataView`, then `inputEncoding` is ignored. + * + * This can be called many times with new data as it is streamed. + * @since v0.1.92 + * @param inputEncoding The `encoding` of the `data` string. + */ + update(data: BinaryLike): Verify; + update(data: string, inputEncoding: Encoding): Verify; + /** + * Verifies the provided data using the given `object` and `signature`. + * + * If `object` is not a `KeyObject`, this function behaves as if `object` had been passed to {@link createPublicKey}. If it is an + * object, the following additional properties can be passed: + * + * The `signature` argument is the previously calculated signature for the data, in + * the `signatureEncoding`. + * If a `signatureEncoding` is specified, the `signature` is expected to be a + * string; otherwise `signature` is expected to be a `Buffer`, `TypedArray`, or `DataView`. + * + * The `verify` object can not be used again after `verify.verify()` has been + * called. Multiple calls to `verify.verify()` will result in an error being + * thrown. + * + * Because public keys can be derived from private keys, a private key may + * be passed instead of a public key. + * @since v0.1.92 + */ + verify( + object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, + signature: NodeJS.ArrayBufferView, + ): boolean; + verify( + object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, + signature: string, + signature_format?: BinaryToTextEncoding, + ): boolean; + } + /** + * Creates a `DiffieHellman` key exchange object using the supplied `prime` and an + * optional specific `generator`. + * + * The `generator` argument can be a number, string, or `Buffer`. If `generator` is not specified, the value `2` is used. + * + * If `primeEncoding` is specified, `prime` is expected to be a string; otherwise + * a `Buffer`, `TypedArray`, or `DataView` is expected. + * + * If `generatorEncoding` is specified, `generator` is expected to be a string; + * otherwise a number, `Buffer`, `TypedArray`, or `DataView` is expected. + * @since v0.11.12 + * @param primeEncoding The `encoding` of the `prime` string. + * @param [generator=2] + * @param generatorEncoding The `encoding` of the `generator` string. + */ + function createDiffieHellman(primeLength: number, generator?: number): DiffieHellman; + function createDiffieHellman( + prime: ArrayBuffer | NodeJS.ArrayBufferView, + generator?: number | ArrayBuffer | NodeJS.ArrayBufferView, + ): DiffieHellman; + function createDiffieHellman( + prime: ArrayBuffer | NodeJS.ArrayBufferView, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator?: number | ArrayBuffer | NodeJS.ArrayBufferView, + ): DiffieHellman; + function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + /** + * The `DiffieHellman` class is a utility for creating Diffie-Hellman key + * exchanges. + * + * Instances of the `DiffieHellman` class can be created using the {@link createDiffieHellman} function. + * + * ```js + * import assert from 'node:assert'; + * + * const { + * createDiffieHellman, + * } = await import('node:crypto'); + * + * // Generate Alice's keys... + * const alice = createDiffieHellman(2048); + * const aliceKey = alice.generateKeys(); + * + * // Generate Bob's keys... + * const bob = createDiffieHellman(alice.getPrime(), alice.getGenerator()); + * const bobKey = bob.generateKeys(); + * + * // Exchange and generate the secret... + * const aliceSecret = alice.computeSecret(bobKey); + * const bobSecret = bob.computeSecret(aliceKey); + * + * // OK + * assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex')); + * ``` + * @since v0.5.0 + */ + class DiffieHellman { + private constructor(); + /** + * Generates private and public Diffie-Hellman key values unless they have been + * generated or computed already, and returns + * the public key in the specified `encoding`. This key should be + * transferred to the other party. + * If `encoding` is provided a string is returned; otherwise a `Buffer` is returned. + * + * This function is a thin wrapper around [`DH_generate_key()`](https://www.openssl.org/docs/man3.0/man3/DH_generate_key.html). In particular, + * once a private key has been generated or set, calling this function only updates + * the public key but does not generate a new private key. + * @since v0.5.0 + * @param encoding The `encoding` of the return value. + */ + generateKeys(): NonSharedBuffer; + generateKeys(encoding: BinaryToTextEncoding): string; + /** + * Computes the shared secret using `otherPublicKey` as the other + * party's public key and returns the computed shared secret. The supplied + * key is interpreted using the specified `inputEncoding`, and secret is + * encoded using specified `outputEncoding`. + * If the `inputEncoding` is not + * provided, `otherPublicKey` is expected to be a `Buffer`, `TypedArray`, or `DataView`. + * + * If `outputEncoding` is given a string is returned; otherwise, a `Buffer` is returned. + * @since v0.5.0 + * @param inputEncoding The `encoding` of an `otherPublicKey` string. + * @param outputEncoding The `encoding` of the return value. + */ + computeSecret( + otherPublicKey: NodeJS.ArrayBufferView, + inputEncoding?: null, + outputEncoding?: null, + ): NonSharedBuffer; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding?: null, + ): NonSharedBuffer; + computeSecret( + otherPublicKey: NodeJS.ArrayBufferView, + inputEncoding: null, + outputEncoding: BinaryToTextEncoding, + ): string; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding: BinaryToTextEncoding, + ): string; + /** + * Returns the Diffie-Hellman prime in the specified `encoding`. + * If `encoding` is provided a string is + * returned; otherwise a `Buffer` is returned. + * @since v0.5.0 + * @param encoding The `encoding` of the return value. + */ + getPrime(): NonSharedBuffer; + getPrime(encoding: BinaryToTextEncoding): string; + /** + * Returns the Diffie-Hellman generator in the specified `encoding`. + * If `encoding` is provided a string is + * returned; otherwise a `Buffer` is returned. + * @since v0.5.0 + * @param encoding The `encoding` of the return value. + */ + getGenerator(): NonSharedBuffer; + getGenerator(encoding: BinaryToTextEncoding): string; + /** + * Returns the Diffie-Hellman public key in the specified `encoding`. + * If `encoding` is provided a + * string is returned; otherwise a `Buffer` is returned. + * @since v0.5.0 + * @param encoding The `encoding` of the return value. + */ + getPublicKey(): NonSharedBuffer; + getPublicKey(encoding: BinaryToTextEncoding): string; + /** + * Returns the Diffie-Hellman private key in the specified `encoding`. + * If `encoding` is provided a + * string is returned; otherwise a `Buffer` is returned. + * @since v0.5.0 + * @param encoding The `encoding` of the return value. + */ + getPrivateKey(): NonSharedBuffer; + getPrivateKey(encoding: BinaryToTextEncoding): string; + /** + * Sets the Diffie-Hellman public key. If the `encoding` argument is provided, `publicKey` is expected + * to be a string. If no `encoding` is provided, `publicKey` is expected + * to be a `Buffer`, `TypedArray`, or `DataView`. + * @since v0.5.0 + * @param encoding The `encoding` of the `publicKey` string. + */ + setPublicKey(publicKey: NodeJS.ArrayBufferView): void; + setPublicKey(publicKey: string, encoding: BufferEncoding): void; + /** + * Sets the Diffie-Hellman private key. If the `encoding` argument is provided,`privateKey` is expected + * to be a string. If no `encoding` is provided, `privateKey` is expected + * to be a `Buffer`, `TypedArray`, or `DataView`. + * + * This function does not automatically compute the associated public key. Either `diffieHellman.setPublicKey()` or `diffieHellman.generateKeys()` can be + * used to manually provide the public key or to automatically derive it. + * @since v0.5.0 + * @param encoding The `encoding` of the `privateKey` string. + */ + setPrivateKey(privateKey: NodeJS.ArrayBufferView): void; + setPrivateKey(privateKey: string, encoding: BufferEncoding): void; + /** + * A bit field containing any warnings and/or errors resulting from a check + * performed during initialization of the `DiffieHellman` object. + * + * The following values are valid for this property (as defined in `node:constants` module): + * + * * `DH_CHECK_P_NOT_SAFE_PRIME` + * * `DH_CHECK_P_NOT_PRIME` + * * `DH_UNABLE_TO_CHECK_GENERATOR` + * * `DH_NOT_SUITABLE_GENERATOR` + * @since v0.11.12 + */ + verifyError: number; + } + /** + * The `DiffieHellmanGroup` class takes a well-known modp group as its argument. + * It works the same as `DiffieHellman`, except that it does not allow changing its keys after creation. + * In other words, it does not implement `setPublicKey()` or `setPrivateKey()` methods. + * + * ```js + * const { createDiffieHellmanGroup } = await import('node:crypto'); + * const dh = createDiffieHellmanGroup('modp1'); + * ``` + * The name (e.g. `'modp1'`) is taken from [RFC 2412](https://www.rfc-editor.org/rfc/rfc2412.txt) (modp1 and 2) and [RFC 3526](https://www.rfc-editor.org/rfc/rfc3526.txt): + * ```bash + * $ perl -ne 'print "$1\n" if /"(modp\d+)"/' src/node_crypto_groups.h + * modp1 # 768 bits + * modp2 # 1024 bits + * modp5 # 1536 bits + * modp14 # 2048 bits + * modp15 # etc. + * modp16 + * modp17 + * modp18 + * ``` + * @since v0.7.5 + */ + const DiffieHellmanGroup: DiffieHellmanGroupConstructor; + interface DiffieHellmanGroupConstructor { + new(name: string): DiffieHellmanGroup; + (name: string): DiffieHellmanGroup; + readonly prototype: DiffieHellmanGroup; + } + type DiffieHellmanGroup = Omit; + /** + * Creates a predefined `DiffieHellmanGroup` key exchange object. The + * supported groups are listed in the documentation for `DiffieHellmanGroup`. + * + * The returned object mimics the interface of objects created by {@link createDiffieHellman}, but will not allow changing + * the keys (with `diffieHellman.setPublicKey()`, for example). The + * advantage of using this method is that the parties do not have to + * generate nor exchange a group modulus beforehand, saving both processor + * and communication time. + * + * Example (obtaining a shared secret): + * + * ```js + * const { + * getDiffieHellman, + * } = await import('node:crypto'); + * const alice = getDiffieHellman('modp14'); + * const bob = getDiffieHellman('modp14'); + * + * alice.generateKeys(); + * bob.generateKeys(); + * + * const aliceSecret = alice.computeSecret(bob.getPublicKey(), null, 'hex'); + * const bobSecret = bob.computeSecret(alice.getPublicKey(), null, 'hex'); + * + * // aliceSecret and bobSecret should be the same + * console.log(aliceSecret === bobSecret); + * ``` + * @since v0.7.5 + */ + function getDiffieHellman(groupName: string): DiffieHellmanGroup; + /** + * An alias for {@link getDiffieHellman} + * @since v0.9.3 + */ + function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; + /** + * Provides an asynchronous Password-Based Key Derivation Function 2 (PBKDF2) + * implementation. A selected HMAC digest algorithm specified by `digest` is + * applied to derive a key of the requested byte length (`keylen`) from the `password`, `salt` and `iterations`. + * + * The supplied `callback` function is called with two arguments: `err` and `derivedKey`. If an error occurs while deriving the key, `err` will be set; + * otherwise `err` will be `null`. By default, the successfully generated `derivedKey` will be passed to the callback as a `Buffer`. An error will be + * thrown if any of the input arguments specify invalid values or types. + * + * The `iterations` argument must be a number set as high as possible. The + * higher the number of iterations, the more secure the derived key will be, + * but will take a longer amount of time to complete. + * + * The `salt` should be as unique as possible. It is recommended that a salt is + * random and at least 16 bytes long. See [NIST SP 800-132](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details. + * + * When passing strings for `password` or `salt`, please consider `caveats when using strings as inputs to cryptographic APIs`. + * + * ```js + * const { + * pbkdf2, + * } = await import('node:crypto'); + * + * pbkdf2('secret', 'salt', 100000, 64, 'sha512', (err, derivedKey) => { + * if (err) throw err; + * console.log(derivedKey.toString('hex')); // '3745e48...08d59ae' + * }); + * ``` + * + * An array of supported digest functions can be retrieved using {@link getHashes}. + * + * This API uses libuv's threadpool, which can have surprising and + * negative performance implications for some applications; see the `UV_THREADPOOL_SIZE` documentation for more information. + * @since v0.5.5 + */ + function pbkdf2( + password: BinaryLike, + salt: BinaryLike, + iterations: number, + keylen: number, + digest: string, + callback: (err: Error | null, derivedKey: NonSharedBuffer) => void, + ): void; + /** + * Provides a synchronous Password-Based Key Derivation Function 2 (PBKDF2) + * implementation. A selected HMAC digest algorithm specified by `digest` is + * applied to derive a key of the requested byte length (`keylen`) from the `password`, `salt` and `iterations`. + * + * If an error occurs an `Error` will be thrown, otherwise the derived key will be + * returned as a `Buffer`. + * + * The `iterations` argument must be a number set as high as possible. The + * higher the number of iterations, the more secure the derived key will be, + * but will take a longer amount of time to complete. + * + * The `salt` should be as unique as possible. It is recommended that a salt is + * random and at least 16 bytes long. See [NIST SP 800-132](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details. + * + * When passing strings for `password` or `salt`, please consider `caveats when using strings as inputs to cryptographic APIs`. + * + * ```js + * const { + * pbkdf2Sync, + * } = await import('node:crypto'); + * + * const key = pbkdf2Sync('secret', 'salt', 100000, 64, 'sha512'); + * console.log(key.toString('hex')); // '3745e48...08d59ae' + * ``` + * + * An array of supported digest functions can be retrieved using {@link getHashes}. + * @since v0.9.3 + */ + function pbkdf2Sync( + password: BinaryLike, + salt: BinaryLike, + iterations: number, + keylen: number, + digest: string, + ): NonSharedBuffer; + /** + * Generates cryptographically strong pseudorandom data. The `size` argument + * is a number indicating the number of bytes to generate. + * + * If a `callback` function is provided, the bytes are generated asynchronously + * and the `callback` function is invoked with two arguments: `err` and `buf`. + * If an error occurs, `err` will be an `Error` object; otherwise it is `null`. The `buf` argument is a `Buffer` containing the generated bytes. + * + * ```js + * // Asynchronous + * const { + * randomBytes, + * } = await import('node:crypto'); + * + * randomBytes(256, (err, buf) => { + * if (err) throw err; + * console.log(`${buf.length} bytes of random data: ${buf.toString('hex')}`); + * }); + * ``` + * + * If the `callback` function is not provided, the random bytes are generated + * synchronously and returned as a `Buffer`. An error will be thrown if + * there is a problem generating the bytes. + * + * ```js + * // Synchronous + * const { + * randomBytes, + * } = await import('node:crypto'); + * + * const buf = randomBytes(256); + * console.log( + * `${buf.length} bytes of random data: ${buf.toString('hex')}`); + * ``` + * + * The `crypto.randomBytes()` method will not complete until there is + * sufficient entropy available. + * This should normally never take longer than a few milliseconds. The only time + * when generating the random bytes may conceivably block for a longer period of + * time is right after boot, when the whole system is still low on entropy. + * + * This API uses libuv's threadpool, which can have surprising and + * negative performance implications for some applications; see the `UV_THREADPOOL_SIZE` documentation for more information. + * + * The asynchronous version of `crypto.randomBytes()` is carried out in a single + * threadpool request. To minimize threadpool task length variation, partition + * large `randomBytes` requests when doing so as part of fulfilling a client + * request. + * @since v0.5.8 + * @param size The number of bytes to generate. The `size` must not be larger than `2**31 - 1`. + * @return if the `callback` function is not provided. + */ + function randomBytes(size: number): NonSharedBuffer; + function randomBytes(size: number, callback: (err: Error | null, buf: NonSharedBuffer) => void): void; + function pseudoRandomBytes(size: number): NonSharedBuffer; + function pseudoRandomBytes(size: number, callback: (err: Error | null, buf: NonSharedBuffer) => void): void; + /** + * Return a random integer `n` such that `min <= n < max`. This + * implementation avoids [modulo bias](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias). + * + * The range (`max - min`) must be less than 2**48. `min` and `max` must + * be [safe integers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger). + * + * If the `callback` function is not provided, the random integer is + * generated synchronously. + * + * ```js + * // Asynchronous + * const { + * randomInt, + * } = await import('node:crypto'); + * + * randomInt(3, (err, n) => { + * if (err) throw err; + * console.log(`Random number chosen from (0, 1, 2): ${n}`); + * }); + * ``` + * + * ```js + * // Synchronous + * const { + * randomInt, + * } = await import('node:crypto'); + * + * const n = randomInt(3); + * console.log(`Random number chosen from (0, 1, 2): ${n}`); + * ``` + * + * ```js + * // With `min` argument + * const { + * randomInt, + * } = await import('node:crypto'); + * + * const n = randomInt(1, 7); + * console.log(`The dice rolled: ${n}`); + * ``` + * @since v14.10.0, v12.19.0 + * @param [min=0] Start of random range (inclusive). + * @param max End of random range (exclusive). + * @param callback `function(err, n) {}`. + */ + function randomInt(max: number): number; + function randomInt(min: number, max: number): number; + function randomInt(max: number, callback: (err: Error | null, value: number) => void): void; + function randomInt(min: number, max: number, callback: (err: Error | null, value: number) => void): void; + /** + * Synchronous version of {@link randomFill}. + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { randomFillSync } = await import('node:crypto'); + * + * const buf = Buffer.alloc(10); + * console.log(randomFillSync(buf).toString('hex')); + * + * randomFillSync(buf, 5); + * console.log(buf.toString('hex')); + * + * // The above is equivalent to the following: + * randomFillSync(buf, 5, 5); + * console.log(buf.toString('hex')); + * ``` + * + * Any `ArrayBuffer`, `TypedArray` or `DataView` instance may be passed as`buffer`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { randomFillSync } = await import('node:crypto'); + * + * const a = new Uint32Array(10); + * console.log(Buffer.from(randomFillSync(a).buffer, + * a.byteOffset, a.byteLength).toString('hex')); + * + * const b = new DataView(new ArrayBuffer(10)); + * console.log(Buffer.from(randomFillSync(b).buffer, + * b.byteOffset, b.byteLength).toString('hex')); + * + * const c = new ArrayBuffer(10); + * console.log(Buffer.from(randomFillSync(c)).toString('hex')); + * ``` + * @since v7.10.0, v6.13.0 + * @param buffer Must be supplied. The size of the provided `buffer` must not be larger than `2**31 - 1`. + * @param [offset=0] + * @param [size=buffer.length - offset] + * @return The object passed as `buffer` argument. + */ + function randomFillSync(buffer: T, offset?: number, size?: number): T; + /** + * This function is similar to {@link randomBytes} but requires the first + * argument to be a `Buffer` that will be filled. It also + * requires that a callback is passed in. + * + * If the `callback` function is not provided, an error will be thrown. + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { randomFill } = await import('node:crypto'); + * + * const buf = Buffer.alloc(10); + * randomFill(buf, (err, buf) => { + * if (err) throw err; + * console.log(buf.toString('hex')); + * }); + * + * randomFill(buf, 5, (err, buf) => { + * if (err) throw err; + * console.log(buf.toString('hex')); + * }); + * + * // The above is equivalent to the following: + * randomFill(buf, 5, 5, (err, buf) => { + * if (err) throw err; + * console.log(buf.toString('hex')); + * }); + * ``` + * + * Any `ArrayBuffer`, `TypedArray`, or `DataView` instance may be passed as `buffer`. + * + * While this includes instances of `Float32Array` and `Float64Array`, this + * function should not be used to generate random floating-point numbers. The + * result may contain `+Infinity`, `-Infinity`, and `NaN`, and even if the array + * contains finite numbers only, they are not drawn from a uniform random + * distribution and have no meaningful lower or upper bounds. + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { randomFill } = await import('node:crypto'); + * + * const a = new Uint32Array(10); + * randomFill(a, (err, buf) => { + * if (err) throw err; + * console.log(Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) + * .toString('hex')); + * }); + * + * const b = new DataView(new ArrayBuffer(10)); + * randomFill(b, (err, buf) => { + * if (err) throw err; + * console.log(Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) + * .toString('hex')); + * }); + * + * const c = new ArrayBuffer(10); + * randomFill(c, (err, buf) => { + * if (err) throw err; + * console.log(Buffer.from(buf).toString('hex')); + * }); + * ``` + * + * This API uses libuv's threadpool, which can have surprising and + * negative performance implications for some applications; see the `UV_THREADPOOL_SIZE` documentation for more information. + * + * The asynchronous version of `crypto.randomFill()` is carried out in a single + * threadpool request. To minimize threadpool task length variation, partition + * large `randomFill` requests when doing so as part of fulfilling a client + * request. + * @since v7.10.0, v6.13.0 + * @param buffer Must be supplied. The size of the provided `buffer` must not be larger than `2**31 - 1`. + * @param [offset=0] + * @param [size=buffer.length - offset] + * @param callback `function(err, buf) {}`. + */ + function randomFill( + buffer: T, + callback: (err: Error | null, buf: T) => void, + ): void; + function randomFill( + buffer: T, + offset: number, + callback: (err: Error | null, buf: T) => void, + ): void; + function randomFill( + buffer: T, + offset: number, + size: number, + callback: (err: Error | null, buf: T) => void, + ): void; + interface ScryptOptions { + cost?: number | undefined; + blockSize?: number | undefined; + parallelization?: number | undefined; + N?: number | undefined; + r?: number | undefined; + p?: number | undefined; + maxmem?: number | undefined; + } + /** + * Provides an asynchronous [scrypt](https://en.wikipedia.org/wiki/Scrypt) implementation. Scrypt is a password-based + * key derivation function that is designed to be expensive computationally and + * memory-wise in order to make brute-force attacks unrewarding. + * + * The `salt` should be as unique as possible. It is recommended that a salt is + * random and at least 16 bytes long. See [NIST SP 800-132](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details. + * + * When passing strings for `password` or `salt`, please consider `caveats when using strings as inputs to cryptographic APIs`. + * + * The `callback` function is called with two arguments: `err` and `derivedKey`. `err` is an exception object when key derivation fails, otherwise `err` is `null`. `derivedKey` is passed to the + * callback as a `Buffer`. + * + * An exception is thrown when any of the input arguments specify invalid values + * or types. + * + * ```js + * const { + * scrypt, + * } = await import('node:crypto'); + * + * // Using the factory defaults. + * scrypt('password', 'salt', 64, (err, derivedKey) => { + * if (err) throw err; + * console.log(derivedKey.toString('hex')); // '3745e48...08d59ae' + * }); + * // Using a custom N parameter. Must be a power of two. + * scrypt('password', 'salt', 64, { N: 1024 }, (err, derivedKey) => { + * if (err) throw err; + * console.log(derivedKey.toString('hex')); // '3745e48...aa39b34' + * }); + * ``` + * @since v10.5.0 + */ + function scrypt( + password: BinaryLike, + salt: BinaryLike, + keylen: number, + callback: (err: Error | null, derivedKey: NonSharedBuffer) => void, + ): void; + function scrypt( + password: BinaryLike, + salt: BinaryLike, + keylen: number, + options: ScryptOptions, + callback: (err: Error | null, derivedKey: NonSharedBuffer) => void, + ): void; + /** + * Provides a synchronous [scrypt](https://en.wikipedia.org/wiki/Scrypt) implementation. Scrypt is a password-based + * key derivation function that is designed to be expensive computationally and + * memory-wise in order to make brute-force attacks unrewarding. + * + * The `salt` should be as unique as possible. It is recommended that a salt is + * random and at least 16 bytes long. See [NIST SP 800-132](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details. + * + * When passing strings for `password` or `salt`, please consider `caveats when using strings as inputs to cryptographic APIs`. + * + * An exception is thrown when key derivation fails, otherwise the derived key is + * returned as a `Buffer`. + * + * An exception is thrown when any of the input arguments specify invalid values + * or types. + * + * ```js + * const { + * scryptSync, + * } = await import('node:crypto'); + * // Using the factory defaults. + * + * const key1 = scryptSync('password', 'salt', 64); + * console.log(key1.toString('hex')); // '3745e48...08d59ae' + * // Using a custom N parameter. Must be a power of two. + * const key2 = scryptSync('password', 'salt', 64, { N: 1024 }); + * console.log(key2.toString('hex')); // '3745e48...aa39b34' + * ``` + * @since v10.5.0 + */ + function scryptSync( + password: BinaryLike, + salt: BinaryLike, + keylen: number, + options?: ScryptOptions, + ): NonSharedBuffer; + interface RsaPublicKey { + key: KeyLike; + padding?: number | undefined; + } + interface RsaPrivateKey { + key: KeyLike; + passphrase?: string | undefined; + /** + * @default 'sha1' + */ + oaepHash?: string | undefined; + oaepLabel?: NodeJS.TypedArray | undefined; + padding?: number | undefined; + } + /** + * Encrypts the content of `buffer` with `key` and returns a new `Buffer` with encrypted content. The returned data can be decrypted using + * the corresponding private key, for example using {@link privateDecrypt}. + * + * If `key` is not a `KeyObject`, this function behaves as if `key` had been passed to {@link createPublicKey}. If it is an + * object, the `padding` property can be passed. Otherwise, this function uses `RSA_PKCS1_OAEP_PADDING`. + * + * Because RSA public keys can be derived from private keys, a private key may + * be passed instead of a public key. + * @since v0.11.14 + */ + function publicEncrypt( + key: RsaPublicKey | RsaPrivateKey | KeyLike, + buffer: NodeJS.ArrayBufferView | string, + ): NonSharedBuffer; + /** + * Decrypts `buffer` with `key`.`buffer` was previously encrypted using + * the corresponding private key, for example using {@link privateEncrypt}. + * + * If `key` is not a `KeyObject`, this function behaves as if `key` had been passed to {@link createPublicKey}. If it is an + * object, the `padding` property can be passed. Otherwise, this function uses `RSA_PKCS1_PADDING`. + * + * Because RSA public keys can be derived from private keys, a private key may + * be passed instead of a public key. + * @since v1.1.0 + */ + function publicDecrypt( + key: RsaPublicKey | RsaPrivateKey | KeyLike, + buffer: NodeJS.ArrayBufferView | string, + ): NonSharedBuffer; + /** + * Decrypts `buffer` with `privateKey`. `buffer` was previously encrypted using + * the corresponding public key, for example using {@link publicEncrypt}. + * + * If `privateKey` is not a `KeyObject`, this function behaves as if `privateKey` had been passed to {@link createPrivateKey}. If it is an + * object, the `padding` property can be passed. Otherwise, this function uses `RSA_PKCS1_OAEP_PADDING`. + * @since v0.11.14 + */ + function privateDecrypt( + privateKey: RsaPrivateKey | KeyLike, + buffer: NodeJS.ArrayBufferView | string, + ): NonSharedBuffer; + /** + * Encrypts `buffer` with `privateKey`. The returned data can be decrypted using + * the corresponding public key, for example using {@link publicDecrypt}. + * + * If `privateKey` is not a `KeyObject`, this function behaves as if `privateKey` had been passed to {@link createPrivateKey}. If it is an + * object, the `padding` property can be passed. Otherwise, this function uses `RSA_PKCS1_PADDING`. + * @since v1.1.0 + */ + function privateEncrypt( + privateKey: RsaPrivateKey | KeyLike, + buffer: NodeJS.ArrayBufferView | string, + ): NonSharedBuffer; + /** + * ```js + * const { + * getCiphers, + * } = await import('node:crypto'); + * + * console.log(getCiphers()); // ['aes-128-cbc', 'aes-128-ccm', ...] + * ``` + * @since v0.9.3 + * @return An array with the names of the supported cipher algorithms. + */ + function getCiphers(): string[]; + /** + * ```js + * const { + * getCurves, + * } = await import('node:crypto'); + * + * console.log(getCurves()); // ['Oakley-EC2N-3', 'Oakley-EC2N-4', ...] + * ``` + * @since v2.3.0 + * @return An array with the names of the supported elliptic curves. + */ + function getCurves(): string[]; + /** + * @since v10.0.0 + * @return `1` if and only if a FIPS compliant crypto provider is currently in use, `0` otherwise. A future semver-major release may change the return type of this API to a {boolean}. + */ + function getFips(): 1 | 0; + /** + * Enables the FIPS compliant crypto provider in a FIPS-enabled Node.js build. + * Throws an error if FIPS mode is not available. + * @since v10.0.0 + * @param bool `true` to enable FIPS mode. + */ + function setFips(bool: boolean): void; + /** + * ```js + * const { + * getHashes, + * } = await import('node:crypto'); + * + * console.log(getHashes()); // ['DSA', 'DSA-SHA', 'DSA-SHA1', ...] + * ``` + * @since v0.9.3 + * @return An array of the names of the supported hash algorithms, such as `'RSA-SHA256'`. Hash algorithms are also called "digest" algorithms. + */ + function getHashes(): string[]; + /** + * The `ECDH` class is a utility for creating Elliptic Curve Diffie-Hellman (ECDH) + * key exchanges. + * + * Instances of the `ECDH` class can be created using the {@link createECDH} function. + * + * ```js + * import assert from 'node:assert'; + * + * const { + * createECDH, + * } = await import('node:crypto'); + * + * // Generate Alice's keys... + * const alice = createECDH('secp521r1'); + * const aliceKey = alice.generateKeys(); + * + * // Generate Bob's keys... + * const bob = createECDH('secp521r1'); + * const bobKey = bob.generateKeys(); + * + * // Exchange and generate the secret... + * const aliceSecret = alice.computeSecret(bobKey); + * const bobSecret = bob.computeSecret(aliceKey); + * + * assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex')); + * // OK + * ``` + * @since v0.11.14 + */ + class ECDH { + private constructor(); + /** + * Converts the EC Diffie-Hellman public key specified by `key` and `curve` to the + * format specified by `format`. The `format` argument specifies point encoding + * and can be `'compressed'`, `'uncompressed'` or `'hybrid'`. The supplied key is + * interpreted using the specified `inputEncoding`, and the returned key is encoded + * using the specified `outputEncoding`. + * + * Use {@link getCurves} to obtain a list of available curve names. + * On recent OpenSSL releases, `openssl ecparam -list_curves` will also display + * the name and description of each available elliptic curve. + * + * If `format` is not specified the point will be returned in `'uncompressed'` format. + * + * If the `inputEncoding` is not provided, `key` is expected to be a `Buffer`, `TypedArray`, or `DataView`. + * + * Example (uncompressing a key): + * + * ```js + * const { + * createECDH, + * ECDH, + * } = await import('node:crypto'); + * + * const ecdh = createECDH('secp256k1'); + * ecdh.generateKeys(); + * + * const compressedKey = ecdh.getPublicKey('hex', 'compressed'); + * + * const uncompressedKey = ECDH.convertKey(compressedKey, + * 'secp256k1', + * 'hex', + * 'hex', + * 'uncompressed'); + * + * // The converted key and the uncompressed public key should be the same + * console.log(uncompressedKey === ecdh.getPublicKey('hex')); + * ``` + * @since v10.0.0 + * @param inputEncoding The `encoding` of the `key` string. + * @param outputEncoding The `encoding` of the return value. + * @param [format='uncompressed'] + */ + static convertKey( + key: BinaryLike, + curve: string, + inputEncoding?: BinaryToTextEncoding, + outputEncoding?: "latin1" | "hex" | "base64" | "base64url", + format?: "uncompressed" | "compressed" | "hybrid", + ): NonSharedBuffer | string; + /** + * Generates private and public EC Diffie-Hellman key values, and returns + * the public key in the specified `format` and `encoding`. This key should be + * transferred to the other party. + * + * The `format` argument specifies point encoding and can be `'compressed'` or `'uncompressed'`. If `format` is not specified, the point will be returned in`'uncompressed'` format. + * + * If `encoding` is provided a string is returned; otherwise a `Buffer` is returned. + * @since v0.11.14 + * @param encoding The `encoding` of the return value. + * @param [format='uncompressed'] + */ + generateKeys(): NonSharedBuffer; + generateKeys(encoding: BinaryToTextEncoding, format?: ECDHKeyFormat): string; + /** + * Computes the shared secret using `otherPublicKey` as the other + * party's public key and returns the computed shared secret. The supplied + * key is interpreted using specified `inputEncoding`, and the returned secret + * is encoded using the specified `outputEncoding`. + * If the `inputEncoding` is not + * provided, `otherPublicKey` is expected to be a `Buffer`, `TypedArray`, or `DataView`. + * + * If `outputEncoding` is given a string will be returned; otherwise a `Buffer` is returned. + * + * `ecdh.computeSecret` will throw an`ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY` error when `otherPublicKey` lies outside of the elliptic curve. Since `otherPublicKey` is + * usually supplied from a remote user over an insecure network, + * be sure to handle this exception accordingly. + * @since v0.11.14 + * @param inputEncoding The `encoding` of the `otherPublicKey` string. + * @param outputEncoding The `encoding` of the return value. + */ + computeSecret(otherPublicKey: NodeJS.ArrayBufferView): NonSharedBuffer; + computeSecret(otherPublicKey: string, inputEncoding: BinaryToTextEncoding): NonSharedBuffer; + computeSecret(otherPublicKey: NodeJS.ArrayBufferView, outputEncoding: BinaryToTextEncoding): string; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding: BinaryToTextEncoding, + ): string; + /** + * If `encoding` is specified, a string is returned; otherwise a `Buffer` is + * returned. + * @since v0.11.14 + * @param encoding The `encoding` of the return value. + * @return The EC Diffie-Hellman in the specified `encoding`. + */ + getPrivateKey(): NonSharedBuffer; + getPrivateKey(encoding: BinaryToTextEncoding): string; + /** + * The `format` argument specifies point encoding and can be `'compressed'` or `'uncompressed'`. If `format` is not specified the point will be returned in`'uncompressed'` format. + * + * If `encoding` is specified, a string is returned; otherwise a `Buffer` is + * returned. + * @since v0.11.14 + * @param encoding The `encoding` of the return value. + * @param [format='uncompressed'] + * @return The EC Diffie-Hellman public key in the specified `encoding` and `format`. + */ + getPublicKey(encoding?: null, format?: ECDHKeyFormat): NonSharedBuffer; + getPublicKey(encoding: BinaryToTextEncoding, format?: ECDHKeyFormat): string; + /** + * Sets the EC Diffie-Hellman private key. + * If `encoding` is provided, `privateKey` is expected + * to be a string; otherwise `privateKey` is expected to be a `Buffer`, `TypedArray`, or `DataView`. + * + * If `privateKey` is not valid for the curve specified when the `ECDH` object was + * created, an error is thrown. Upon setting the private key, the associated + * public point (key) is also generated and set in the `ECDH` object. + * @since v0.11.14 + * @param encoding The `encoding` of the `privateKey` string. + */ + setPrivateKey(privateKey: NodeJS.ArrayBufferView): void; + setPrivateKey(privateKey: string, encoding: BinaryToTextEncoding): void; + } + /** + * Creates an Elliptic Curve Diffie-Hellman (`ECDH`) key exchange object using a + * predefined curve specified by the `curveName` string. Use {@link getCurves} to obtain a list of available curve names. On recent + * OpenSSL releases, `openssl ecparam -list_curves` will also display the name + * and description of each available elliptic curve. + * @since v0.11.14 + */ + function createECDH(curveName: string): ECDH; + /** + * This function compares the underlying bytes that represent the given `ArrayBuffer`, `TypedArray`, or `DataView` instances using a constant-time + * algorithm. + * + * This function does not leak timing information that + * would allow an attacker to guess one of the values. This is suitable for + * comparing HMAC digests or secret values like authentication cookies or [capability urls](https://www.w3.org/TR/capability-urls/). + * + * `a` and `b` must both be `Buffer`s, `TypedArray`s, or `DataView`s, and they + * must have the same byte length. An error is thrown if `a` and `b` have + * different byte lengths. + * + * If at least one of `a` and `b` is a `TypedArray` with more than one byte per + * entry, such as `Uint16Array`, the result will be computed using the platform + * byte order. + * + * **When both of the inputs are `Float32Array`s or `Float64Array`s, this function might return unexpected results due to IEEE 754** + * **encoding of floating-point numbers. In particular, neither `x === y` nor `Object.is(x, y)` implies that the byte representations of two floating-point** + * **numbers `x` and `y` are equal.** + * + * Use of `crypto.timingSafeEqual` does not guarantee that the _surrounding_ code + * is timing-safe. Care should be taken to ensure that the surrounding code does + * not introduce timing vulnerabilities. + * @since v6.6.0 + */ + function timingSafeEqual(a: NodeJS.ArrayBufferView, b: NodeJS.ArrayBufferView): boolean; + type KeyType = "rsa" | "rsa-pss" | "dsa" | "ec" | "ed25519" | "ed448" | "x25519" | "x448"; + type KeyFormat = "pem" | "der" | "jwk"; + interface BasePrivateKeyEncodingOptions { + format: T; + cipher?: string | undefined; + passphrase?: string | undefined; + } + interface KeyPairKeyObjectResult { + publicKey: KeyObject; + privateKey: KeyObject; + } + interface ED25519KeyPairKeyObjectOptions {} + interface ED448KeyPairKeyObjectOptions {} + interface X25519KeyPairKeyObjectOptions {} + interface X448KeyPairKeyObjectOptions {} + interface ECKeyPairKeyObjectOptions { + /** + * Name of the curve to use + */ + namedCurve: string; + /** + * Must be `'named'` or `'explicit'`. Default: `'named'`. + */ + paramEncoding?: "explicit" | "named" | undefined; + } + interface RSAKeyPairKeyObjectOptions { + /** + * Key size in bits + */ + modulusLength: number; + /** + * Public exponent + * @default 0x10001 + */ + publicExponent?: number | undefined; + } + interface RSAPSSKeyPairKeyObjectOptions { + /** + * Key size in bits + */ + modulusLength: number; + /** + * Public exponent + * @default 0x10001 + */ + publicExponent?: number | undefined; + /** + * Name of the message digest + */ + hashAlgorithm?: string | undefined; + /** + * Name of the message digest used by MGF1 + */ + mgf1HashAlgorithm?: string | undefined; + /** + * Minimal salt length in bytes + */ + saltLength?: string | undefined; + } + interface DSAKeyPairKeyObjectOptions { + /** + * Key size in bits + */ + modulusLength: number; + /** + * Size of q in bits + */ + divisorLength: number; + } + interface RSAKeyPairOptions { + /** + * Key size in bits + */ + modulusLength: number; + /** + * Public exponent + * @default 0x10001 + */ + publicExponent?: number | undefined; + publicKeyEncoding: { + type: "pkcs1" | "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "pkcs1" | "pkcs8"; + }; + } + interface RSAPSSKeyPairOptions { + /** + * Key size in bits + */ + modulusLength: number; + /** + * Public exponent + * @default 0x10001 + */ + publicExponent?: number | undefined; + /** + * Name of the message digest + */ + hashAlgorithm?: string | undefined; + /** + * Name of the message digest used by MGF1 + */ + mgf1HashAlgorithm?: string | undefined; + /** + * Minimal salt length in bytes + */ + saltLength?: string | undefined; + publicKeyEncoding: { + type: "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "pkcs8"; + }; + } + interface DSAKeyPairOptions { + /** + * Key size in bits + */ + modulusLength: number; + /** + * Size of q in bits + */ + divisorLength: number; + publicKeyEncoding: { + type: "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "pkcs8"; + }; + } + interface ECKeyPairOptions extends ECKeyPairKeyObjectOptions { + publicKeyEncoding: { + type: "pkcs1" | "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "sec1" | "pkcs8"; + }; + } + interface ED25519KeyPairOptions { + publicKeyEncoding: { + type: "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "pkcs8"; + }; + } + interface ED448KeyPairOptions { + publicKeyEncoding: { + type: "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "pkcs8"; + }; + } + interface X25519KeyPairOptions { + publicKeyEncoding: { + type: "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "pkcs8"; + }; + } + interface X448KeyPairOptions { + publicKeyEncoding: { + type: "spki"; + format: PubF; + }; + privateKeyEncoding: BasePrivateKeyEncodingOptions & { + type: "pkcs8"; + }; + } + interface KeyPairSyncResult { + publicKey: T1; + privateKey: T2; + } + /** + * Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC, + * Ed25519, Ed448, X25519, X448, and DH are currently supported. + * + * If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function + * behaves as if `keyObject.export()` had been called on its result. Otherwise, + * the respective part of the key is returned as a `KeyObject`. + * + * When encoding public keys, it is recommended to use `'spki'`. When encoding + * private keys, it is recommended to use `'pkcs8'` with a strong passphrase, + * and to keep the passphrase confidential. + * + * ```js + * const { + * generateKeyPairSync, + * } = await import('node:crypto'); + * + * const { + * publicKey, + * privateKey, + * } = generateKeyPairSync('rsa', { + * modulusLength: 4096, + * publicKeyEncoding: { + * type: 'spki', + * format: 'pem', + * }, + * privateKeyEncoding: { + * type: 'pkcs8', + * format: 'pem', + * cipher: 'aes-256-cbc', + * passphrase: 'top secret', + * }, + * }); + * ``` + * + * The return value `{ publicKey, privateKey }` represents the generated key pair. + * When PEM encoding was selected, the respective key will be a string, otherwise + * it will be a buffer containing the data encoded as DER. + * @since v10.12.0 + * @param type Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. + */ + function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "rsa", options: RSAKeyPairKeyObjectOptions): KeyPairKeyObjectResult; + function generateKeyPairSync( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "rsa-pss", options: RSAPSSKeyPairKeyObjectOptions): KeyPairKeyObjectResult; + function generateKeyPairSync( + type: "dsa", + options: DSAKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "dsa", + options: DSAKeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "dsa", + options: DSAKeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "dsa", + options: DSAKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "dsa", options: DSAKeyPairKeyObjectOptions): KeyPairKeyObjectResult; + function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "ec", options: ECKeyPairKeyObjectOptions): KeyPairKeyObjectResult; + function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "ed25519", options?: ED25519KeyPairKeyObjectOptions): KeyPairKeyObjectResult; + function generateKeyPairSync( + type: "ed448", + options: ED448KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ed448", + options: ED448KeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ed448", + options: ED448KeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "ed448", + options: ED448KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "ed448", options?: ED448KeyPairKeyObjectOptions): KeyPairKeyObjectResult; + function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "x25519", options?: X25519KeyPairKeyObjectOptions): KeyPairKeyObjectResult; + function generateKeyPairSync( + type: "x448", + options: X448KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "x448", + options: X448KeyPairOptions<"pem", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "x448", + options: X448KeyPairOptions<"der", "pem">, + ): KeyPairSyncResult; + function generateKeyPairSync( + type: "x448", + options: X448KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + function generateKeyPairSync(type: "x448", options?: X448KeyPairKeyObjectOptions): KeyPairKeyObjectResult; + /** + * Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC, + * Ed25519, Ed448, X25519, X448, and DH are currently supported. + * + * If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function + * behaves as if `keyObject.export()` had been called on its result. Otherwise, + * the respective part of the key is returned as a `KeyObject`. + * + * It is recommended to encode public keys as `'spki'` and private keys as `'pkcs8'` with encryption for long-term storage: + * + * ```js + * const { + * generateKeyPair, + * } = await import('node:crypto'); + * + * generateKeyPair('rsa', { + * modulusLength: 4096, + * publicKeyEncoding: { + * type: 'spki', + * format: 'pem', + * }, + * privateKeyEncoding: { + * type: 'pkcs8', + * format: 'pem', + * cipher: 'aes-256-cbc', + * passphrase: 'top secret', + * }, + * }, (err, publicKey, privateKey) => { + * // Handle errors and use the generated key pair. + * }); + * ``` + * + * On completion, `callback` will be called with `err` set to `undefined` and `publicKey` / `privateKey` representing the generated key pair. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a `Promise` for an `Object` with `publicKey` and `privateKey` properties. + * @since v10.12.0 + * @param type Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. + */ + function generateKeyPair( + type: "rsa", + options: RSAKeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "rsa", + options: RSAKeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "rsa", + options: RSAKeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "rsa", + options: RSAKeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "rsa", + options: RSAKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + function generateKeyPair( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "rsa-pss", + options: RSAPSSKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + function generateKeyPair( + type: "dsa", + options: DSAKeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "dsa", + options: DSAKeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "dsa", + options: DSAKeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "dsa", + options: DSAKeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "dsa", + options: DSAKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + function generateKeyPair( + type: "ec", + options: ECKeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "ec", + options: ECKeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "ec", + options: ECKeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "ec", + options: ECKeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "ec", + options: ECKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + function generateKeyPair( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "ed25519", + options: ED25519KeyPairKeyObjectOptions | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + function generateKeyPair( + type: "ed448", + options: ED448KeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "ed448", + options: ED448KeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "ed448", + options: ED448KeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "ed448", + options: ED448KeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "ed448", + options: ED448KeyPairKeyObjectOptions | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + function generateKeyPair( + type: "x25519", + options: X25519KeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "x25519", + options: X25519KeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "x25519", + options: X25519KeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "x25519", + options: X25519KeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "x25519", + options: X25519KeyPairKeyObjectOptions | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + function generateKeyPair( + type: "x448", + options: X448KeyPairOptions<"pem", "pem">, + callback: (err: Error | null, publicKey: string, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "x448", + options: X448KeyPairOptions<"pem", "der">, + callback: (err: Error | null, publicKey: string, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "x448", + options: X448KeyPairOptions<"der", "pem">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: string) => void, + ): void; + function generateKeyPair( + type: "x448", + options: X448KeyPairOptions<"der", "der">, + callback: (err: Error | null, publicKey: NonSharedBuffer, privateKey: NonSharedBuffer) => void, + ): void; + function generateKeyPair( + type: "x448", + options: X448KeyPairKeyObjectOptions | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + namespace generateKeyPair { + function __promisify__( + type: "rsa", + options: RSAKeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "rsa", + options: RSAKeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "rsa", + options: RSAKeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "rsa", + options: RSAKeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__(type: "rsa", options: RSAKeyPairKeyObjectOptions): Promise; + function __promisify__( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "rsa-pss", + options: RSAPSSKeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "rsa-pss", + options: RSAPSSKeyPairKeyObjectOptions, + ): Promise; + function __promisify__( + type: "dsa", + options: DSAKeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "dsa", + options: DSAKeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "dsa", + options: DSAKeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "dsa", + options: DSAKeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__(type: "dsa", options: DSAKeyPairKeyObjectOptions): Promise; + function __promisify__( + type: "ec", + options: ECKeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "ec", + options: ECKeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "ec", + options: ECKeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "ec", + options: ECKeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__(type: "ec", options: ECKeyPairKeyObjectOptions): Promise; + function __promisify__( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "ed25519", + options?: ED25519KeyPairKeyObjectOptions, + ): Promise; + function __promisify__( + type: "ed448", + options: ED448KeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "ed448", + options: ED448KeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "ed448", + options: ED448KeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "ed448", + options: ED448KeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__(type: "ed448", options?: ED448KeyPairKeyObjectOptions): Promise; + function __promisify__( + type: "x25519", + options: X25519KeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "x25519", + options: X25519KeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "x25519", + options: X25519KeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "x25519", + options: X25519KeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "x25519", + options?: X25519KeyPairKeyObjectOptions, + ): Promise; + function __promisify__( + type: "x448", + options: X448KeyPairOptions<"pem", "pem">, + ): Promise<{ + publicKey: string; + privateKey: string; + }>; + function __promisify__( + type: "x448", + options: X448KeyPairOptions<"pem", "der">, + ): Promise<{ + publicKey: string; + privateKey: NonSharedBuffer; + }>; + function __promisify__( + type: "x448", + options: X448KeyPairOptions<"der", "pem">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: string; + }>; + function __promisify__( + type: "x448", + options: X448KeyPairOptions<"der", "der">, + ): Promise<{ + publicKey: NonSharedBuffer; + privateKey: NonSharedBuffer; + }>; + function __promisify__(type: "x448", options?: X448KeyPairKeyObjectOptions): Promise; + } + /** + * Calculates and returns the signature for `data` using the given private key and + * algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is + * dependent upon the key type (especially Ed25519 and Ed448). + * + * If `key` is not a `KeyObject`, this function behaves as if `key` had been + * passed to {@link createPrivateKey}. If it is an object, the following + * additional properties can be passed: + * + * If the `callback` function is provided this function uses libuv's threadpool. + * @since v12.0.0 + */ + function sign( + algorithm: string | null | undefined, + data: NodeJS.ArrayBufferView, + key: KeyLike | SignKeyObjectInput | SignPrivateKeyInput | SignJsonWebKeyInput, + ): NonSharedBuffer; + function sign( + algorithm: string | null | undefined, + data: NodeJS.ArrayBufferView, + key: KeyLike | SignKeyObjectInput | SignPrivateKeyInput | SignJsonWebKeyInput, + callback: (error: Error | null, data: NonSharedBuffer) => void, + ): void; + /** + * Verifies the given signature for `data` using the given key and algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is dependent upon the + * key type (especially Ed25519 and Ed448). + * + * If `key` is not a `KeyObject`, this function behaves as if `key` had been + * passed to {@link createPublicKey}. If it is an object, the following + * additional properties can be passed: + * + * The `signature` argument is the previously calculated signature for the `data`. + * + * Because public keys can be derived from private keys, a private key or a public + * key may be passed for `key`. + * + * If the `callback` function is provided this function uses libuv's threadpool. + * @since v12.0.0 + */ + function verify( + algorithm: string | null | undefined, + data: NodeJS.ArrayBufferView, + key: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, + signature: NodeJS.ArrayBufferView, + ): boolean; + function verify( + algorithm: string | null | undefined, + data: NodeJS.ArrayBufferView, + key: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, + signature: NodeJS.ArrayBufferView, + callback: (error: Error | null, result: boolean) => void, + ): void; + /** + * Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`. + * Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'` (for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES). + * @since v13.9.0, v12.17.0 + */ + function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): NonSharedBuffer; + /** + * A utility for creating one-shot hash digests of data. It can be faster than the object-based `crypto.createHash()` when hashing a smaller amount of data + * (<= 5MB) that's readily available. If the data can be big or if it is streamed, it's still recommended to use `crypto.createHash()` instead. The `algorithm` + * is dependent on the available algorithms supported by the version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. On recent releases + * of OpenSSL, `openssl list -digest-algorithms` will display the available digest algorithms. + * + * Example: + * + * ```js + * import crypto from 'node:crypto'; + * import { Buffer } from 'node:buffer'; + * + * // Hashing a string and return the result as a hex-encoded string. + * const string = 'Node.js'; + * // 10b3493287f831e81a438811a1ffba01f8cec4b7 + * console.log(crypto.hash('sha1', string)); + * + * // Encode a base64-encoded string into a Buffer, hash it and return + * // the result as a buffer. + * const base64 = 'Tm9kZS5qcw=='; + * // + * console.log(crypto.hash('sha1', Buffer.from(base64, 'base64'), 'buffer')); + * ``` + * @since v21.7.0, v20.12.0 + * @param data When `data` is a string, it will be encoded as UTF-8 before being hashed. If a different input encoding is desired for a string input, user + * could encode the string into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing the encoded `TypedArray` into this API instead. + * @param [outputEncoding='hex'] [Encoding](https://nodejs.org/docs/latest-v22.x/api/buffer.html#buffers-and-character-encodings) used to encode the returned digest. + */ + function hash(algorithm: string, data: BinaryLike, outputEncoding?: BinaryToTextEncoding): string; + function hash(algorithm: string, data: BinaryLike, outputEncoding: "buffer"): NonSharedBuffer; + function hash( + algorithm: string, + data: BinaryLike, + outputEncoding?: BinaryToTextEncoding | "buffer", + ): string | NonSharedBuffer; + type CipherMode = "cbc" | "ccm" | "cfb" | "ctr" | "ecb" | "gcm" | "ocb" | "ofb" | "stream" | "wrap" | "xts"; + interface CipherInfoOptions { + /** + * A test key length. + */ + keyLength?: number | undefined; + /** + * A test IV length. + */ + ivLength?: number | undefined; + } + interface CipherInfo { + /** + * The name of the cipher. + */ + name: string; + /** + * The nid of the cipher. + */ + nid: number; + /** + * The block size of the cipher in bytes. + * This property is omitted when mode is 'stream'. + */ + blockSize?: number | undefined; + /** + * The expected or default initialization vector length in bytes. + * This property is omitted if the cipher does not use an initialization vector. + */ + ivLength?: number | undefined; + /** + * The expected or default key length in bytes. + */ + keyLength: number; + /** + * The cipher mode. + */ + mode: CipherMode; + } + /** + * Returns information about a given cipher. + * + * Some ciphers accept variable length keys and initialization vectors. By default, + * the `crypto.getCipherInfo()` method will return the default values for these + * ciphers. To test if a given key length or iv length is acceptable for given + * cipher, use the `keyLength` and `ivLength` options. If the given values are + * unacceptable, `undefined` will be returned. + * @since v15.0.0 + * @param nameOrNid The name or nid of the cipher to query. + */ + function getCipherInfo(nameOrNid: string | number, options?: CipherInfoOptions): CipherInfo | undefined; + /** + * HKDF is a simple key derivation function defined in RFC 5869\. The given `ikm`, `salt` and `info` are used with the `digest` to derive a key of `keylen` bytes. + * + * The supplied `callback` function is called with two arguments: `err` and `derivedKey`. If an errors occurs while deriving the key, `err` will be set; + * otherwise `err` will be `null`. The successfully generated `derivedKey` will + * be passed to the callback as an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). An error will be thrown if any + * of the input arguments specify invalid values or types. + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { + * hkdf, + * } = await import('node:crypto'); + * + * hkdf('sha512', 'key', 'salt', 'info', 64, (err, derivedKey) => { + * if (err) throw err; + * console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653' + * }); + * ``` + * @since v15.0.0 + * @param digest The digest algorithm to use. + * @param ikm The input keying material. Must be provided but can be zero-length. + * @param salt The salt value. Must be provided but can be zero-length. + * @param info Additional info value. Must be provided but can be zero-length, and cannot be more than 1024 bytes. + * @param keylen The length of the key to generate. Must be greater than 0. The maximum allowable value is `255` times the number of bytes produced by the selected digest function (e.g. `sha512` + * generates 64-byte hashes, making the maximum HKDF output 16320 bytes). + */ + function hkdf( + digest: string, + irm: BinaryLike | KeyObject, + salt: BinaryLike, + info: BinaryLike, + keylen: number, + callback: (err: Error | null, derivedKey: ArrayBuffer) => void, + ): void; + /** + * Provides a synchronous HKDF key derivation function as defined in RFC 5869\. The + * given `ikm`, `salt` and `info` are used with the `digest` to derive a key of `keylen` bytes. + * + * The successfully generated `derivedKey` will be returned as an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). + * + * An error will be thrown if any of the input arguments specify invalid values or + * types, or if the derived key cannot be generated. + * + * ```js + * import { Buffer } from 'node:buffer'; + * const { + * hkdfSync, + * } = await import('node:crypto'); + * + * const derivedKey = hkdfSync('sha512', 'key', 'salt', 'info', 64); + * console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653' + * ``` + * @since v15.0.0 + * @param digest The digest algorithm to use. + * @param ikm The input keying material. Must be provided but can be zero-length. + * @param salt The salt value. Must be provided but can be zero-length. + * @param info Additional info value. Must be provided but can be zero-length, and cannot be more than 1024 bytes. + * @param keylen The length of the key to generate. Must be greater than 0. The maximum allowable value is `255` times the number of bytes produced by the selected digest function (e.g. `sha512` + * generates 64-byte hashes, making the maximum HKDF output 16320 bytes). + */ + function hkdfSync( + digest: string, + ikm: BinaryLike | KeyObject, + salt: BinaryLike, + info: BinaryLike, + keylen: number, + ): ArrayBuffer; + interface SecureHeapUsage { + /** + * The total allocated secure heap size as specified using the `--secure-heap=n` command-line flag. + */ + total: number; + /** + * The minimum allocation from the secure heap as specified using the `--secure-heap-min` command-line flag. + */ + min: number; + /** + * The total number of bytes currently allocated from the secure heap. + */ + used: number; + /** + * The calculated ratio of `used` to `total` allocated bytes. + */ + utilization: number; + } + /** + * @since v15.6.0 + */ + function secureHeapUsed(): SecureHeapUsage; + interface RandomUUIDOptions { + /** + * By default, to improve performance, + * Node.js will pre-emptively generate and persistently cache enough + * random data to generate up to 128 random UUIDs. To generate a UUID + * without using the cache, set `disableEntropyCache` to `true`. + * + * @default `false` + */ + disableEntropyCache?: boolean | undefined; + } + type UUID = `${string}-${string}-${string}-${string}-${string}`; + /** + * Generates a random [RFC 4122](https://www.rfc-editor.org/rfc/rfc4122.txt) version 4 UUID. The UUID is generated using a + * cryptographic pseudorandom number generator. + * @since v15.6.0, v14.17.0 + */ + function randomUUID(options?: RandomUUIDOptions): UUID; + interface X509CheckOptions { + /** + * @default 'always' + */ + subject?: "always" | "default" | "never" | undefined; + /** + * @default true + */ + wildcards?: boolean | undefined; + /** + * @default true + */ + partialWildcards?: boolean | undefined; + /** + * @default false + */ + multiLabelWildcards?: boolean | undefined; + /** + * @default false + */ + singleLabelSubdomains?: boolean | undefined; + } + /** + * Encapsulates an X509 certificate and provides read-only access to + * its information. + * + * ```js + * const { X509Certificate } = await import('node:crypto'); + * + * const x509 = new X509Certificate('{... pem encoded cert ...}'); + * + * console.log(x509.subject); + * ``` + * @since v15.6.0 + */ + class X509Certificate { + /** + * Will be \`true\` if this is a Certificate Authority (CA) certificate. + * @since v15.6.0 + */ + readonly ca: boolean; + /** + * The SHA-1 fingerprint of this certificate. + * + * Because SHA-1 is cryptographically broken and because the security of SHA-1 is + * significantly worse than that of algorithms that are commonly used to sign + * certificates, consider using `x509.fingerprint256` instead. + * @since v15.6.0 + */ + readonly fingerprint: string; + /** + * The SHA-256 fingerprint of this certificate. + * @since v15.6.0 + */ + readonly fingerprint256: string; + /** + * The SHA-512 fingerprint of this certificate. + * + * Because computing the SHA-256 fingerprint is usually faster and because it is + * only half the size of the SHA-512 fingerprint, `x509.fingerprint256` may be + * a better choice. While SHA-512 presumably provides a higher level of security in + * general, the security of SHA-256 matches that of most algorithms that are + * commonly used to sign certificates. + * @since v17.2.0, v16.14.0 + */ + readonly fingerprint512: string; + /** + * The complete subject of this certificate. + * @since v15.6.0 + */ + readonly subject: string; + /** + * The subject alternative name specified for this certificate. + * + * This is a comma-separated list of subject alternative names. Each entry begins + * with a string identifying the kind of the subject alternative name followed by + * a colon and the value associated with the entry. + * + * Earlier versions of Node.js incorrectly assumed that it is safe to split this + * property at the two-character sequence `', '` (see [CVE-2021-44532](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532)). However, + * both malicious and legitimate certificates can contain subject alternative names + * that include this sequence when represented as a string. + * + * After the prefix denoting the type of the entry, the remainder of each entry + * might be enclosed in quotes to indicate that the value is a JSON string literal. + * For backward compatibility, Node.js only uses JSON string literals within this + * property when necessary to avoid ambiguity. Third-party code should be prepared + * to handle both possible entry formats. + * @since v15.6.0 + */ + readonly subjectAltName: string | undefined; + /** + * A textual representation of the certificate's authority information access + * extension. + * + * This is a line feed separated list of access descriptions. Each line begins with + * the access method and the kind of the access location, followed by a colon and + * the value associated with the access location. + * + * After the prefix denoting the access method and the kind of the access location, + * the remainder of each line might be enclosed in quotes to indicate that the + * value is a JSON string literal. For backward compatibility, Node.js only uses + * JSON string literals within this property when necessary to avoid ambiguity. + * Third-party code should be prepared to handle both possible entry formats. + * @since v15.6.0 + */ + readonly infoAccess: string | undefined; + /** + * An array detailing the key usages for this certificate. + * @since v15.6.0 + */ + readonly keyUsage: string[]; + /** + * The issuer identification included in this certificate. + * @since v15.6.0 + */ + readonly issuer: string; + /** + * The issuer certificate or `undefined` if the issuer certificate is not + * available. + * @since v15.9.0 + */ + readonly issuerCertificate: X509Certificate | undefined; + /** + * The public key `KeyObject` for this certificate. + * @since v15.6.0 + */ + readonly publicKey: KeyObject; + /** + * A `Buffer` containing the DER encoding of this certificate. + * @since v15.6.0 + */ + readonly raw: NonSharedBuffer; + /** + * The serial number of this certificate. + * + * Serial numbers are assigned by certificate authorities and do not uniquely + * identify certificates. Consider using `x509.fingerprint256` as a unique + * identifier instead. + * @since v15.6.0 + */ + readonly serialNumber: string; + /** + * The date/time from which this certificate is considered valid. + * @since v15.6.0 + */ + readonly validFrom: string; + /** + * The date/time from which this certificate is valid, encapsulated in a `Date` object. + * @since v22.10.0 + */ + readonly validFromDate: Date; + /** + * The date/time until which this certificate is considered valid. + * @since v15.6.0 + */ + readonly validTo: string; + /** + * The date/time until which this certificate is valid, encapsulated in a `Date` object. + * @since v22.10.0 + */ + readonly validToDate: Date; + constructor(buffer: BinaryLike); + /** + * Checks whether the certificate matches the given email address. + * + * If the `'subject'` option is undefined or set to `'default'`, the certificate + * subject is only considered if the subject alternative name extension either does + * not exist or does not contain any email addresses. + * + * If the `'subject'` option is set to `'always'` and if the subject alternative + * name extension either does not exist or does not contain a matching email + * address, the certificate subject is considered. + * + * If the `'subject'` option is set to `'never'`, the certificate subject is never + * considered, even if the certificate contains no subject alternative names. + * @since v15.6.0 + * @return Returns `email` if the certificate matches, `undefined` if it does not. + */ + checkEmail(email: string, options?: Pick): string | undefined; + /** + * Checks whether the certificate matches the given host name. + * + * If the certificate matches the given host name, the matching subject name is + * returned. The returned name might be an exact match (e.g., `foo.example.com`) + * or it might contain wildcards (e.g., `*.example.com`). Because host name + * comparisons are case-insensitive, the returned subject name might also differ + * from the given `name` in capitalization. + * + * If the `'subject'` option is undefined or set to `'default'`, the certificate + * subject is only considered if the subject alternative name extension either does + * not exist or does not contain any DNS names. This behavior is consistent with [RFC 2818](https://www.rfc-editor.org/rfc/rfc2818.txt) ("HTTP Over TLS"). + * + * If the `'subject'` option is set to `'always'` and if the subject alternative + * name extension either does not exist or does not contain a matching DNS name, + * the certificate subject is considered. + * + * If the `'subject'` option is set to `'never'`, the certificate subject is never + * considered, even if the certificate contains no subject alternative names. + * @since v15.6.0 + * @return Returns a subject name that matches `name`, or `undefined` if no subject name matches `name`. + */ + checkHost(name: string, options?: X509CheckOptions): string | undefined; + /** + * Checks whether the certificate matches the given IP address (IPv4 or IPv6). + * + * Only [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280.txt) `iPAddress` subject alternative names are considered, and they + * must match the given `ip` address exactly. Other subject alternative names as + * well as the subject field of the certificate are ignored. + * @since v15.6.0 + * @return Returns `ip` if the certificate matches, `undefined` if it does not. + */ + checkIP(ip: string): string | undefined; + /** + * Checks whether this certificate was potentially issued by the given `otherCert` + * by comparing the certificate metadata. + * + * This is useful for pruning a list of possible issuer certificates which have been + * selected using a more rudimentary filtering routine, i.e. just based on subject + * and issuer names. + * + * Finally, to verify that this certificate's signature was produced by a private key + * corresponding to `otherCert`'s public key use `x509.verify(publicKey)` + * with `otherCert`'s public key represented as a `KeyObject` + * like so + * + * ```js + * if (!x509.verify(otherCert.publicKey)) { + * throw new Error('otherCert did not issue x509'); + * } + * ``` + * @since v15.6.0 + */ + checkIssued(otherCert: X509Certificate): boolean; + /** + * Checks whether the public key for this certificate is consistent with + * the given private key. + * @since v15.6.0 + * @param privateKey A private key. + */ + checkPrivateKey(privateKey: KeyObject): boolean; + /** + * There is no standard JSON encoding for X509 certificates. The`toJSON()` method returns a string containing the PEM encoded + * certificate. + * @since v15.6.0 + */ + toJSON(): string; + /** + * Returns information about this certificate using the legacy `certificate object` encoding. + * @since v15.6.0 + */ + toLegacyObject(): PeerCertificate; + /** + * Returns the PEM-encoded certificate. + * @since v15.6.0 + */ + toString(): string; + /** + * Verifies that this certificate was signed by the given public key. + * Does not perform any other validation checks on the certificate. + * @since v15.6.0 + * @param publicKey A public key. + */ + verify(publicKey: KeyObject): boolean; + } + type LargeNumberLike = NodeJS.ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint; + interface GeneratePrimeOptions { + add?: LargeNumberLike | undefined; + rem?: LargeNumberLike | undefined; + /** + * @default false + */ + safe?: boolean | undefined; + bigint?: boolean | undefined; + } + interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions { + bigint: true; + } + interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions { + bigint?: false | undefined; + } + /** + * Generates a pseudorandom prime of `size` bits. + * + * If `options.safe` is `true`, the prime will be a safe prime -- that is, `(prime - 1) / 2` will also be a prime. + * + * The `options.add` and `options.rem` parameters can be used to enforce additional + * requirements, e.g., for Diffie-Hellman: + * + * * If `options.add` and `options.rem` are both set, the prime will satisfy the + * condition that `prime % add = rem`. + * * If only `options.add` is set and `options.safe` is not `true`, the prime will + * satisfy the condition that `prime % add = 1`. + * * If only `options.add` is set and `options.safe` is set to `true`, the prime + * will instead satisfy the condition that `prime % add = 3`. This is necessary + * because `prime % add = 1` for `options.add > 2` would contradict the condition + * enforced by `options.safe`. + * * `options.rem` is ignored if `options.add` is not given. + * + * Both `options.add` and `options.rem` must be encoded as big-endian sequences + * if given as an `ArrayBuffer`, `SharedArrayBuffer`, `TypedArray`, `Buffer`, or `DataView`. + * + * By default, the prime is encoded as a big-endian sequence of octets + * in an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). If the `bigint` option is `true`, then a + * [bigint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) is provided. + * @since v15.8.0 + * @param size The size (in bits) of the prime to generate. + */ + function generatePrime(size: number, callback: (err: Error | null, prime: ArrayBuffer) => void): void; + function generatePrime( + size: number, + options: GeneratePrimeOptionsBigInt, + callback: (err: Error | null, prime: bigint) => void, + ): void; + function generatePrime( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + function generatePrime( + size: number, + options: GeneratePrimeOptions, + callback: (err: Error | null, prime: ArrayBuffer | bigint) => void, + ): void; + /** + * Generates a pseudorandom prime of `size` bits. + * + * If `options.safe` is `true`, the prime will be a safe prime -- that is, `(prime - 1) / 2` will also be a prime. + * + * The `options.add` and `options.rem` parameters can be used to enforce additional + * requirements, e.g., for Diffie-Hellman: + * + * * If `options.add` and `options.rem` are both set, the prime will satisfy the + * condition that `prime % add = rem`. + * * If only `options.add` is set and `options.safe` is not `true`, the prime will + * satisfy the condition that `prime % add = 1`. + * * If only `options.add` is set and `options.safe` is set to `true`, the prime + * will instead satisfy the condition that `prime % add = 3`. This is necessary + * because `prime % add = 1` for `options.add > 2` would contradict the condition + * enforced by `options.safe`. + * * `options.rem` is ignored if `options.add` is not given. + * + * Both `options.add` and `options.rem` must be encoded as big-endian sequences + * if given as an `ArrayBuffer`, `SharedArrayBuffer`, `TypedArray`, `Buffer`, or `DataView`. + * + * By default, the prime is encoded as a big-endian sequence of octets + * in an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). If the `bigint` option is `true`, then a + * [bigint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) is provided. + * @since v15.8.0 + * @param size The size (in bits) of the prime to generate. + */ + function generatePrimeSync(size: number): ArrayBuffer; + function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint; + function generatePrimeSync(size: number, options: GeneratePrimeOptionsArrayBuffer): ArrayBuffer; + function generatePrimeSync(size: number, options: GeneratePrimeOptions): ArrayBuffer | bigint; + interface CheckPrimeOptions { + /** + * The number of Miller-Rabin probabilistic primality iterations to perform. + * When the value is 0 (zero), a number of checks is used that yields a false positive rate of at most `2**-64` for random input. + * Care must be used when selecting a number of checks. + * Refer to the OpenSSL documentation for the BN_is_prime_ex function nchecks options for more details. + * + * @default 0 + */ + checks?: number | undefined; + } + /** + * Checks the primality of the `candidate`. + * @since v15.8.0 + * @param candidate A possible prime encoded as a sequence of big endian octets of arbitrary length. + */ + function checkPrime(value: LargeNumberLike, callback: (err: Error | null, result: boolean) => void): void; + function checkPrime( + value: LargeNumberLike, + options: CheckPrimeOptions, + callback: (err: Error | null, result: boolean) => void, + ): void; + /** + * Checks the primality of the `candidate`. + * @since v15.8.0 + * @param candidate A possible prime encoded as a sequence of big endian octets of arbitrary length. + * @return `true` if the candidate is a prime with an error probability less than `0.25 ** options.checks`. + */ + function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean; + /** + * Load and set the `engine` for some or all OpenSSL functions (selected by flags). + * + * `engine` could be either an id or a path to the engine's shared library. + * + * The optional `flags` argument uses `ENGINE_METHOD_ALL` by default. The `flags` is a bit field taking one of or a mix of the following flags (defined in `crypto.constants`): + * + * * `crypto.constants.ENGINE_METHOD_RSA` + * * `crypto.constants.ENGINE_METHOD_DSA` + * * `crypto.constants.ENGINE_METHOD_DH` + * * `crypto.constants.ENGINE_METHOD_RAND` + * * `crypto.constants.ENGINE_METHOD_EC` + * * `crypto.constants.ENGINE_METHOD_CIPHERS` + * * `crypto.constants.ENGINE_METHOD_DIGESTS` + * * `crypto.constants.ENGINE_METHOD_PKEY_METHS` + * * `crypto.constants.ENGINE_METHOD_PKEY_ASN1_METHS` + * * `crypto.constants.ENGINE_METHOD_ALL` + * * `crypto.constants.ENGINE_METHOD_NONE` + * @since v0.11.11 + * @param flags + */ + function setEngine(engine: string, flags?: number): void; + /** + * A convenient alias for {@link webcrypto.getRandomValues}. This + * implementation is not compliant with the Web Crypto spec, to write + * web-compatible code use {@link webcrypto.getRandomValues} instead. + * @since v17.4.0 + * @return Returns `typedArray`. + */ + function getRandomValues(typedArray: T): T; + /** + * A convenient alias for `crypto.webcrypto.subtle`. + * @since v17.4.0 + */ + const subtle: webcrypto.SubtleCrypto; + /** + * An implementation of the Web Crypto API standard. + * + * See the {@link https://nodejs.org/docs/latest/api/webcrypto.html Web Crypto API documentation} for details. + * @since v15.0.0 + */ + const webcrypto: webcrypto.Crypto; + namespace webcrypto { + type BufferSource = ArrayBufferView | ArrayBuffer; + type KeyFormat = "jwk" | "pkcs8" | "raw" | "spki"; + type KeyType = "private" | "public" | "secret"; + type KeyUsage = + | "decrypt" + | "deriveBits" + | "deriveKey" + | "encrypt" + | "sign" + | "unwrapKey" + | "verify" + | "wrapKey"; + type AlgorithmIdentifier = Algorithm | string; + type HashAlgorithmIdentifier = AlgorithmIdentifier; + type NamedCurve = string; + type BigInteger = Uint8Array; + interface AesCbcParams extends Algorithm { + iv: BufferSource; + } + interface AesCtrParams extends Algorithm { + counter: BufferSource; + length: number; + } + interface AesDerivedKeyParams extends Algorithm { + length: number; + } + interface AesGcmParams extends Algorithm { + additionalData?: BufferSource; + iv: BufferSource; + tagLength?: number; + } + interface AesKeyAlgorithm extends KeyAlgorithm { + length: number; + } + interface AesKeyGenParams extends Algorithm { + length: number; + } + interface Algorithm { + name: string; + } + interface EcKeyAlgorithm extends KeyAlgorithm { + namedCurve: NamedCurve; + } + interface EcKeyGenParams extends Algorithm { + namedCurve: NamedCurve; + } + interface EcKeyImportParams extends Algorithm { + namedCurve: NamedCurve; + } + interface EcdhKeyDeriveParams extends Algorithm { + public: CryptoKey; + } + interface EcdsaParams extends Algorithm { + hash: HashAlgorithmIdentifier; + } + interface Ed448Params extends Algorithm { + context?: BufferSource; + } + interface HkdfParams extends Algorithm { + hash: HashAlgorithmIdentifier; + info: BufferSource; + salt: BufferSource; + } + interface HmacImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; + } + interface HmacKeyAlgorithm extends KeyAlgorithm { + hash: KeyAlgorithm; + length: number; + } + interface HmacKeyGenParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; + } + interface JsonWebKey { + alg?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + ext?: boolean; + k?: string; + key_ops?: string[]; + kty?: string; + n?: string; + oth?: RsaOtherPrimesInfo[]; + p?: string; + q?: string; + qi?: string; + use?: string; + x?: string; + y?: string; + } + interface KeyAlgorithm { + name: string; + } + interface Pbkdf2Params extends Algorithm { + hash: HashAlgorithmIdentifier; + iterations: number; + salt: BufferSource; + } + interface RsaHashedImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + } + interface RsaHashedKeyAlgorithm extends RsaKeyAlgorithm { + hash: KeyAlgorithm; + } + interface RsaHashedKeyGenParams extends RsaKeyGenParams { + hash: HashAlgorithmIdentifier; + } + interface RsaKeyAlgorithm extends KeyAlgorithm { + modulusLength: number; + publicExponent: BigInteger; + } + interface RsaKeyGenParams extends Algorithm { + modulusLength: number; + publicExponent: BigInteger; + } + interface RsaOaepParams extends Algorithm { + label?: BufferSource; + } + interface RsaOtherPrimesInfo { + d?: string; + r?: string; + t?: string; + } + interface RsaPssParams extends Algorithm { + saltLength: number; + } + /** + * Importing the `webcrypto` object (`import { webcrypto } from 'node:crypto'`) gives an instance of the `Crypto` class. + * `Crypto` is a singleton that provides access to the remainder of the crypto API. + * @since v15.0.0 + */ + interface Crypto { + /** + * Provides access to the `SubtleCrypto` API. + * @since v15.0.0 + */ + readonly subtle: SubtleCrypto; + /** + * Generates cryptographically strong random values. + * The given `typedArray` is filled with random values, and a reference to `typedArray` is returned. + * + * The given `typedArray` must be an integer-based instance of {@link NodeJS.TypedArray}, i.e. `Float32Array` and `Float64Array` are not accepted. + * + * An error will be thrown if the given `typedArray` is larger than 65,536 bytes. + * @since v15.0.0 + */ + getRandomValues>(typedArray: T): T; + /** + * Generates a random {@link https://www.rfc-editor.org/rfc/rfc4122.txt RFC 4122} version 4 UUID. + * The UUID is generated using a cryptographic pseudorandom number generator. + * @since v16.7.0 + */ + randomUUID(): UUID; + CryptoKey: CryptoKeyConstructor; + } + // This constructor throws ILLEGAL_CONSTRUCTOR so it should not be newable. + interface CryptoKeyConstructor { + /** Illegal constructor */ + (_: { readonly _: unique symbol }): never; // Allows instanceof to work but not be callable by the user. + readonly length: 0; + readonly name: "CryptoKey"; + readonly prototype: CryptoKey; + } + /** + * @since v15.0.0 + */ + interface CryptoKey { + /** + * An object detailing the algorithm for which the key can be used along with additional algorithm-specific parameters. + * @since v15.0.0 + */ + readonly algorithm: KeyAlgorithm; + /** + * When `true`, the {@link CryptoKey} can be extracted using either `subtleCrypto.exportKey()` or `subtleCrypto.wrapKey()`. + * @since v15.0.0 + */ + readonly extractable: boolean; + /** + * A string identifying whether the key is a symmetric (`'secret'`) or asymmetric (`'private'` or `'public'`) key. + * @since v15.0.0 + */ + readonly type: KeyType; + /** + * An array of strings identifying the operations for which the key may be used. + * + * The possible usages are: + * - `'encrypt'` - The key may be used to encrypt data. + * - `'decrypt'` - The key may be used to decrypt data. + * - `'sign'` - The key may be used to generate digital signatures. + * - `'verify'` - The key may be used to verify digital signatures. + * - `'deriveKey'` - The key may be used to derive a new key. + * - `'deriveBits'` - The key may be used to derive bits. + * - `'wrapKey'` - The key may be used to wrap another key. + * - `'unwrapKey'` - The key may be used to unwrap another key. + * + * Valid key usages depend on the key algorithm (identified by `cryptokey.algorithm.name`). + * @since v15.0.0 + */ + readonly usages: KeyUsage[]; + } + /** + * The `CryptoKeyPair` is a simple dictionary object with `publicKey` and `privateKey` properties, representing an asymmetric key pair. + * @since v15.0.0 + */ + interface CryptoKeyPair { + /** + * A {@link CryptoKey} whose type will be `'private'`. + * @since v15.0.0 + */ + privateKey: CryptoKey; + /** + * A {@link CryptoKey} whose type will be `'public'`. + * @since v15.0.0 + */ + publicKey: CryptoKey; + } + /** + * @since v15.0.0 + */ + interface SubtleCrypto { + /** + * Using the method and parameters specified in `algorithm` and the keying material provided by `key`, + * `subtle.decrypt()` attempts to decipher the provided `data`. If successful, + * the returned promise will be resolved with an `` containing the plaintext result. + * + * The algorithms currently supported include: + * + * - `'RSA-OAEP'` + * - `'AES-CTR'` + * - `'AES-CBC'` + * - `'AES-GCM'` + * @since v15.0.0 + */ + decrypt( + algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + /** + * Using the method and parameters specified in `algorithm` and the keying material provided by `baseKey`, + * `subtle.deriveBits()` attempts to generate `length` bits. + * The Node.js implementation requires that when `length` is a number it must be multiple of `8`. + * When `length` is `null` the maximum number of bits for a given algorithm is generated. This is allowed + * for the `'ECDH'`, `'X25519'`, and `'X448'` algorithms. + * If successful, the returned promise will be resolved with an `` containing the generated data. + * + * The algorithms currently supported include: + * + * - `'ECDH'` + * - `'X25519'` + * - `'X448'` + * - `'HKDF'` + * - `'PBKDF2'` + * @since v15.0.0 + */ + deriveBits( + algorithm: EcdhKeyDeriveParams, + baseKey: CryptoKey, + length?: number | null, + ): Promise; + deriveBits( + algorithm: EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, + baseKey: CryptoKey, + length: number, + ): Promise; + /** + * Using the method and parameters specified in `algorithm`, and the keying material provided by `baseKey`, + * `subtle.deriveKey()` attempts to generate a new ` based on the method and parameters in `derivedKeyAlgorithm`. + * + * Calling `subtle.deriveKey()` is equivalent to calling `subtle.deriveBits()` to generate raw keying material, + * then passing the result into the `subtle.importKey()` method using the `deriveKeyAlgorithm`, `extractable`, and `keyUsages` parameters as input. + * + * The algorithms currently supported include: + * + * - `'ECDH'` + * - `'X25519'` + * - `'X448'` + * - `'HKDF'` + * - `'PBKDF2'` + * @param keyUsages See {@link https://nodejs.org/docs/latest/api/webcrypto.html#cryptokeyusages Key usages}. + * @since v15.0.0 + */ + deriveKey( + algorithm: EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, + baseKey: CryptoKey, + derivedKeyAlgorithm: AlgorithmIdentifier | HmacImportParams | AesDerivedKeyParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + /** + * Using the method identified by `algorithm`, `subtle.digest()` attempts to generate a digest of `data`. + * If successful, the returned promise is resolved with an `` containing the computed digest. + * + * If `algorithm` is provided as a ``, it must be one of: + * + * - `'SHA-1'` + * - `'SHA-256'` + * - `'SHA-384'` + * - `'SHA-512'` + * + * If `algorithm` is provided as an ``, it must have a `name` property whose value is one of the above. + * @since v15.0.0 + */ + digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + /** + * Using the method and parameters specified by `algorithm` and the keying material provided by `key`, + * `subtle.encrypt()` attempts to encipher `data`. If successful, + * the returned promise is resolved with an `` containing the encrypted result. + * + * The algorithms currently supported include: + * + * - `'RSA-OAEP'` + * - `'AES-CTR'` + * - `'AES-CBC'` + * - `'AES-GCM'` + * @since v15.0.0 + */ + encrypt( + algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + /** + * Exports the given key into the specified format, if supported. + * + * If the `` is not extractable, the returned promise will reject. + * + * When `format` is either `'pkcs8'` or `'spki'` and the export is successful, + * the returned promise will be resolved with an `` containing the exported key data. + * + * When `format` is `'jwk'` and the export is successful, the returned promise will be resolved with a + * JavaScript object conforming to the {@link https://tools.ietf.org/html/rfc7517 JSON Web Key} specification. + * @param format Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. + * @returns `` containing ``. + * @since v15.0.0 + */ + exportKey(format: "jwk", key: CryptoKey): Promise; + exportKey(format: Exclude, key: CryptoKey): Promise; + /** + * Using the method and parameters provided in `algorithm`, + * `subtle.generateKey()` attempts to generate new keying material. + * Depending the method used, the method may generate either a single `` or a ``. + * + * The `` (public and private key) generating algorithms supported include: + * + * - `'RSASSA-PKCS1-v1_5'` + * - `'RSA-PSS'` + * - `'RSA-OAEP'` + * - `'ECDSA'` + * - `'Ed25519'` + * - `'Ed448'` + * - `'ECDH'` + * - `'X25519'` + * - `'X448'` + * The `` (secret key) generating algorithms supported include: + * + * - `'HMAC'` + * - `'AES-CTR'` + * - `'AES-CBC'` + * - `'AES-GCM'` + * - `'AES-KW'` + * @param keyUsages See {@link https://nodejs.org/docs/latest/api/webcrypto.html#cryptokeyusages Key usages}. + * @since v15.0.0 + */ + generateKey( + algorithm: RsaHashedKeyGenParams | EcKeyGenParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + generateKey( + algorithm: AesKeyGenParams | HmacKeyGenParams | Pbkdf2Params, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + generateKey( + algorithm: AlgorithmIdentifier, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise; + /** + * The `subtle.importKey()` method attempts to interpret the provided `keyData` as the given `format` + * to create a `` instance using the provided `algorithm`, `extractable`, and `keyUsages` arguments. + * If the import is successful, the returned promise will be resolved with the created ``. + * + * If importing a `'PBKDF2'` key, `extractable` must be `false`. + * @param format Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. + * @param keyUsages See {@link https://nodejs.org/docs/latest/api/webcrypto.html#cryptokeyusages Key usages}. + * @since v15.0.0 + */ + importKey( + format: "jwk", + keyData: JsonWebKey, + algorithm: + | AlgorithmIdentifier + | RsaHashedImportParams + | EcKeyImportParams + | HmacImportParams + | AesKeyAlgorithm, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + importKey( + format: Exclude, + keyData: BufferSource, + algorithm: + | AlgorithmIdentifier + | RsaHashedImportParams + | EcKeyImportParams + | HmacImportParams + | AesKeyAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise; + /** + * Using the method and parameters given by `algorithm` and the keying material provided by `key`, + * `subtle.sign()` attempts to generate a cryptographic signature of `data`. If successful, + * the returned promise is resolved with an `` containing the generated signature. + * + * The algorithms currently supported include: + * + * - `'RSASSA-PKCS1-v1_5'` + * - `'RSA-PSS'` + * - `'ECDSA'` + * - `'Ed25519'` + * - `'Ed448'` + * - `'HMAC'` + * @since v15.0.0 + */ + sign( + algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams | Ed448Params, + key: CryptoKey, + data: BufferSource, + ): Promise; + /** + * In cryptography, "wrapping a key" refers to exporting and then encrypting the keying material. + * The `subtle.unwrapKey()` method attempts to decrypt a wrapped key and create a `` instance. + * It is equivalent to calling `subtle.decrypt()` first on the encrypted key data (using the `wrappedKey`, `unwrapAlgo`, and `unwrappingKey` arguments as input) + * then passing the results in to the `subtle.importKey()` method using the `unwrappedKeyAlgo`, `extractable`, and `keyUsages` arguments as inputs. + * If successful, the returned promise is resolved with a `` object. + * + * The wrapping algorithms currently supported include: + * + * - `'RSA-OAEP'` + * - `'AES-CTR'` + * - `'AES-CBC'` + * - `'AES-GCM'` + * - `'AES-KW'` + * + * The unwrapped key algorithms supported include: + * + * - `'RSASSA-PKCS1-v1_5'` + * - `'RSA-PSS'` + * - `'RSA-OAEP'` + * - `'ECDSA'` + * - `'Ed25519'` + * - `'Ed448'` + * - `'ECDH'` + * - `'X25519'` + * - `'X448'` + * - `'HMAC'` + * - `'AES-CTR'` + * - `'AES-CBC'` + * - `'AES-GCM'` + * - `'AES-KW'` + * @param format Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. + * @param keyUsages See {@link https://nodejs.org/docs/latest/api/webcrypto.html#cryptokeyusages Key usages}. + * @since v15.0.0 + */ + unwrapKey( + format: KeyFormat, + wrappedKey: BufferSource, + unwrappingKey: CryptoKey, + unwrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, + unwrappedKeyAlgorithm: + | AlgorithmIdentifier + | RsaHashedImportParams + | EcKeyImportParams + | HmacImportParams + | AesKeyAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise; + /** + * Using the method and parameters given in `algorithm` and the keying material provided by `key`, + * `subtle.verify()` attempts to verify that `signature` is a valid cryptographic signature of `data`. + * The returned promise is resolved with either `true` or `false`. + * + * The algorithms currently supported include: + * + * - `'RSASSA-PKCS1-v1_5'` + * - `'RSA-PSS'` + * - `'ECDSA'` + * - `'Ed25519'` + * - `'Ed448'` + * - `'HMAC'` + * @since v15.0.0 + */ + verify( + algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams | Ed448Params, + key: CryptoKey, + signature: BufferSource, + data: BufferSource, + ): Promise; + /** + * In cryptography, "wrapping a key" refers to exporting and then encrypting the keying material. + * The `subtle.wrapKey()` method exports the keying material into the format identified by `format`, + * then encrypts it using the method and parameters specified by `wrapAlgo` and the keying material provided by `wrappingKey`. + * It is the equivalent to calling `subtle.exportKey()` using `format` and `key` as the arguments, + * then passing the result to the `subtle.encrypt()` method using `wrappingKey` and `wrapAlgo` as inputs. + * If successful, the returned promise will be resolved with an `` containing the encrypted key data. + * + * The wrapping algorithms currently supported include: + * + * - `'RSA-OAEP'` + * - `'AES-CTR'` + * - `'AES-CBC'` + * - `'AES-GCM'` + * - `'AES-KW'` + * @param format Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. + * @since v15.0.0 + */ + wrapKey( + format: KeyFormat, + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, + ): Promise; + } + } + + global { + var crypto: typeof globalThis extends { + crypto: infer T; + onmessage: any; + } ? T + : webcrypto.Crypto; + } +} +declare module "node:crypto" { + export * from "crypto"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dgram.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dgram.d.ts new file mode 100644 index 00000000..9776de0d --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dgram.d.ts @@ -0,0 +1,600 @@ +/** + * The `node:dgram` module provides an implementation of UDP datagram sockets. + * + * ```js + * import dgram from 'node:dgram'; + * + * const server = dgram.createSocket('udp4'); + * + * server.on('error', (err) => { + * console.error(`server error:\n${err.stack}`); + * server.close(); + * }); + * + * server.on('message', (msg, rinfo) => { + * console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`); + * }); + * + * server.on('listening', () => { + * const address = server.address(); + * console.log(`server listening ${address.address}:${address.port}`); + * }); + * + * server.bind(41234); + * // Prints: server listening 0.0.0.0:41234 + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/dgram.js) + */ +declare module "dgram" { + import { NonSharedBuffer } from "node:buffer"; + import { AddressInfo, BlockList } from "node:net"; + import * as dns from "node:dns"; + import { Abortable, EventEmitter } from "node:events"; + interface RemoteInfo { + address: string; + family: "IPv4" | "IPv6"; + port: number; + size: number; + } + interface BindOptions { + port?: number | undefined; + address?: string | undefined; + exclusive?: boolean | undefined; + fd?: number | undefined; + } + type SocketType = "udp4" | "udp6"; + interface SocketOptions extends Abortable { + type: SocketType; + reuseAddr?: boolean | undefined; + reusePort?: boolean | undefined; + /** + * @default false + */ + ipv6Only?: boolean | undefined; + recvBufferSize?: number | undefined; + sendBufferSize?: number | undefined; + lookup?: + | (( + hostname: string, + options: dns.LookupOneOptions, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ) => void) + | undefined; + receiveBlockList?: BlockList | undefined; + sendBlockList?: BlockList | undefined; + } + /** + * Creates a `dgram.Socket` object. Once the socket is created, calling `socket.bind()` will instruct the socket to begin listening for datagram + * messages. When `address` and `port` are not passed to `socket.bind()` the + * method will bind the socket to the "all interfaces" address on a random port + * (it does the right thing for both `udp4` and `udp6` sockets). The bound address + * and port can be retrieved using `socket.address().address` and `socket.address().port`. + * + * If the `signal` option is enabled, calling `.abort()` on the corresponding `AbortController` is similar to calling `.close()` on the socket: + * + * ```js + * const controller = new AbortController(); + * const { signal } = controller; + * const server = dgram.createSocket({ type: 'udp4', signal }); + * server.on('message', (msg, rinfo) => { + * console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`); + * }); + * // Later, when you want to close the server. + * controller.abort(); + * ``` + * @since v0.11.13 + * @param options Available options are: + * @param callback Attached as a listener for `'message'` events. Optional. + */ + function createSocket(type: SocketType, callback?: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): Socket; + function createSocket(options: SocketOptions, callback?: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): Socket; + /** + * Encapsulates the datagram functionality. + * + * New instances of `dgram.Socket` are created using {@link createSocket}. + * The `new` keyword is not to be used to create `dgram.Socket` instances. + * @since v0.1.99 + */ + class Socket extends EventEmitter { + /** + * Tells the kernel to join a multicast group at the given `multicastAddress` and `multicastInterface` using the `IP_ADD_MEMBERSHIP` socket option. If the `multicastInterface` argument is not + * specified, the operating system will choose + * one interface and will add membership to it. To add membership to every + * available interface, call `addMembership` multiple times, once per interface. + * + * When called on an unbound socket, this method will implicitly bind to a random + * port, listening on all interfaces. + * + * When sharing a UDP socket across multiple `cluster` workers, the`socket.addMembership()` function must be called only once or an`EADDRINUSE` error will occur: + * + * ```js + * import cluster from 'node:cluster'; + * import dgram from 'node:dgram'; + * + * if (cluster.isPrimary) { + * cluster.fork(); // Works ok. + * cluster.fork(); // Fails with EADDRINUSE. + * } else { + * const s = dgram.createSocket('udp4'); + * s.bind(1234, () => { + * s.addMembership('224.0.0.114'); + * }); + * } + * ``` + * @since v0.6.9 + */ + addMembership(multicastAddress: string, multicastInterface?: string): void; + /** + * Returns an object containing the address information for a socket. + * For UDP sockets, this object will contain `address`, `family`, and `port` properties. + * + * This method throws `EBADF` if called on an unbound socket. + * @since v0.1.99 + */ + address(): AddressInfo; + /** + * For UDP sockets, causes the `dgram.Socket` to listen for datagram + * messages on a named `port` and optional `address`. If `port` is not + * specified or is `0`, the operating system will attempt to bind to a + * random port. If `address` is not specified, the operating system will + * attempt to listen on all addresses. Once binding is complete, a `'listening'` event is emitted and the optional `callback` function is + * called. + * + * Specifying both a `'listening'` event listener and passing a `callback` to the `socket.bind()` method is not harmful but not very + * useful. + * + * A bound datagram socket keeps the Node.js process running to receive + * datagram messages. + * + * If binding fails, an `'error'` event is generated. In rare case (e.g. + * attempting to bind with a closed socket), an `Error` may be thrown. + * + * Example of a UDP server listening on port 41234: + * + * ```js + * import dgram from 'node:dgram'; + * + * const server = dgram.createSocket('udp4'); + * + * server.on('error', (err) => { + * console.error(`server error:\n${err.stack}`); + * server.close(); + * }); + * + * server.on('message', (msg, rinfo) => { + * console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`); + * }); + * + * server.on('listening', () => { + * const address = server.address(); + * console.log(`server listening ${address.address}:${address.port}`); + * }); + * + * server.bind(41234); + * // Prints: server listening 0.0.0.0:41234 + * ``` + * @since v0.1.99 + * @param callback with no parameters. Called when binding is complete. + */ + bind(port?: number, address?: string, callback?: () => void): this; + bind(port?: number, callback?: () => void): this; + bind(callback?: () => void): this; + bind(options: BindOptions, callback?: () => void): this; + /** + * Close the underlying socket and stop listening for data on it. If a callback is + * provided, it is added as a listener for the `'close'` event. + * @since v0.1.99 + * @param callback Called when the socket has been closed. + */ + close(callback?: () => void): this; + /** + * Associates the `dgram.Socket` to a remote address and port. Every + * message sent by this handle is automatically sent to that destination. Also, + * the socket will only receive messages from that remote peer. + * Trying to call `connect()` on an already connected socket will result + * in an `ERR_SOCKET_DGRAM_IS_CONNECTED` exception. If `address` is not + * provided, `'127.0.0.1'` (for `udp4` sockets) or `'::1'` (for `udp6` sockets) + * will be used by default. Once the connection is complete, a `'connect'` event + * is emitted and the optional `callback` function is called. In case of failure, + * the `callback` is called or, failing this, an `'error'` event is emitted. + * @since v12.0.0 + * @param callback Called when the connection is completed or on error. + */ + connect(port: number, address?: string, callback?: () => void): void; + connect(port: number, callback: () => void): void; + /** + * A synchronous function that disassociates a connected `dgram.Socket` from + * its remote address. Trying to call `disconnect()` on an unbound or already + * disconnected socket will result in an `ERR_SOCKET_DGRAM_NOT_CONNECTED` exception. + * @since v12.0.0 + */ + disconnect(): void; + /** + * Instructs the kernel to leave a multicast group at `multicastAddress` using the `IP_DROP_MEMBERSHIP` socket option. This method is automatically called by the + * kernel when the socket is closed or the process terminates, so most apps will + * never have reason to call this. + * + * If `multicastInterface` is not specified, the operating system will attempt to + * drop membership on all valid interfaces. + * @since v0.6.9 + */ + dropMembership(multicastAddress: string, multicastInterface?: string): void; + /** + * This method throws `ERR_SOCKET_BUFFER_SIZE` if called on an unbound socket. + * @since v8.7.0 + * @return the `SO_RCVBUF` socket receive buffer size in bytes. + */ + getRecvBufferSize(): number; + /** + * This method throws `ERR_SOCKET_BUFFER_SIZE` if called on an unbound socket. + * @since v8.7.0 + * @return the `SO_SNDBUF` socket send buffer size in bytes. + */ + getSendBufferSize(): number; + /** + * @since v18.8.0, v16.19.0 + * @return Number of bytes queued for sending. + */ + getSendQueueSize(): number; + /** + * @since v18.8.0, v16.19.0 + * @return Number of send requests currently in the queue awaiting to be processed. + */ + getSendQueueCount(): number; + /** + * By default, binding a socket will cause it to block the Node.js process from + * exiting as long as the socket is open. The `socket.unref()` method can be used + * to exclude the socket from the reference counting that keeps the Node.js + * process active. The `socket.ref()` method adds the socket back to the reference + * counting and restores the default behavior. + * + * Calling `socket.ref()` multiples times will have no additional effect. + * + * The `socket.ref()` method returns a reference to the socket so calls can be + * chained. + * @since v0.9.1 + */ + ref(): this; + /** + * Returns an object containing the `address`, `family`, and `port` of the remote + * endpoint. This method throws an `ERR_SOCKET_DGRAM_NOT_CONNECTED` exception + * if the socket is not connected. + * @since v12.0.0 + */ + remoteAddress(): AddressInfo; + /** + * Broadcasts a datagram on the socket. + * For connectionless sockets, the destination `port` and `address` must be + * specified. Connected sockets, on the other hand, will use their associated + * remote endpoint, so the `port` and `address` arguments must not be set. + * + * The `msg` argument contains the message to be sent. + * Depending on its type, different behavior can apply. If `msg` is a `Buffer`, + * any `TypedArray` or a `DataView`, + * the `offset` and `length` specify the offset within the `Buffer` where the + * message begins and the number of bytes in the message, respectively. + * If `msg` is a `String`, then it is automatically converted to a `Buffer` with `'utf8'` encoding. With messages that + * contain multi-byte characters, `offset` and `length` will be calculated with + * respect to `byte length` and not the character position. + * If `msg` is an array, `offset` and `length` must not be specified. + * + * The `address` argument is a string. If the value of `address` is a host name, + * DNS will be used to resolve the address of the host. If `address` is not + * provided or otherwise nullish, `'127.0.0.1'` (for `udp4` sockets) or `'::1'` (for `udp6` sockets) will be used by default. + * + * If the socket has not been previously bound with a call to `bind`, the socket + * is assigned a random port number and is bound to the "all interfaces" address + * (`'0.0.0.0'` for `udp4` sockets, `'::0'` for `udp6` sockets.) + * + * An optional `callback` function may be specified to as a way of reporting + * DNS errors or for determining when it is safe to reuse the `buf` object. + * DNS lookups delay the time to send for at least one tick of the + * Node.js event loop. + * + * The only way to know for sure that the datagram has been sent is by using a `callback`. If an error occurs and a `callback` is given, the error will be + * passed as the first argument to the `callback`. If a `callback` is not given, + * the error is emitted as an `'error'` event on the `socket` object. + * + * Offset and length are optional but both _must_ be set if either are used. + * They are supported only when the first argument is a `Buffer`, a `TypedArray`, + * or a `DataView`. + * + * This method throws `ERR_SOCKET_BAD_PORT` if called on an unbound socket. + * + * Example of sending a UDP packet to a port on `localhost`; + * + * ```js + * import dgram from 'node:dgram'; + * import { Buffer } from 'node:buffer'; + * + * const message = Buffer.from('Some bytes'); + * const client = dgram.createSocket('udp4'); + * client.send(message, 41234, 'localhost', (err) => { + * client.close(); + * }); + * ``` + * + * Example of sending a UDP packet composed of multiple buffers to a port on`127.0.0.1`; + * + * ```js + * import dgram from 'node:dgram'; + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('Some '); + * const buf2 = Buffer.from('bytes'); + * const client = dgram.createSocket('udp4'); + * client.send([buf1, buf2], 41234, (err) => { + * client.close(); + * }); + * ``` + * + * Sending multiple buffers might be faster or slower depending on the + * application and operating system. Run benchmarks to + * determine the optimal strategy on a case-by-case basis. Generally speaking, + * however, sending multiple buffers is faster. + * + * Example of sending a UDP packet using a socket connected to a port on `localhost`: + * + * ```js + * import dgram from 'node:dgram'; + * import { Buffer } from 'node:buffer'; + * + * const message = Buffer.from('Some bytes'); + * const client = dgram.createSocket('udp4'); + * client.connect(41234, 'localhost', (err) => { + * client.send(message, (err) => { + * client.close(); + * }); + * }); + * ``` + * @since v0.1.99 + * @param msg Message to be sent. + * @param offset Offset in the buffer where the message starts. + * @param length Number of bytes in the message. + * @param port Destination port. + * @param address Destination host name or IP address. + * @param callback Called when the message has been sent. + */ + send( + msg: string | NodeJS.ArrayBufferView | readonly any[], + port?: number, + address?: string, + callback?: (error: Error | null, bytes: number) => void, + ): void; + send( + msg: string | NodeJS.ArrayBufferView | readonly any[], + port?: number, + callback?: (error: Error | null, bytes: number) => void, + ): void; + send( + msg: string | NodeJS.ArrayBufferView | readonly any[], + callback?: (error: Error | null, bytes: number) => void, + ): void; + send( + msg: string | NodeJS.ArrayBufferView, + offset: number, + length: number, + port?: number, + address?: string, + callback?: (error: Error | null, bytes: number) => void, + ): void; + send( + msg: string | NodeJS.ArrayBufferView, + offset: number, + length: number, + port?: number, + callback?: (error: Error | null, bytes: number) => void, + ): void; + send( + msg: string | NodeJS.ArrayBufferView, + offset: number, + length: number, + callback?: (error: Error | null, bytes: number) => void, + ): void; + /** + * Sets or clears the `SO_BROADCAST` socket option. When set to `true`, UDP + * packets may be sent to a local interface's broadcast address. + * + * This method throws `EBADF` if called on an unbound socket. + * @since v0.6.9 + */ + setBroadcast(flag: boolean): void; + /** + * _All references to scope in this section are referring to [IPv6 Zone Indices](https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses), which are defined by [RFC + * 4007](https://tools.ietf.org/html/rfc4007). In string form, an IP_ + * _with a scope index is written as `'IP%scope'` where scope is an interface name_ + * _or interface number._ + * + * Sets the default outgoing multicast interface of the socket to a chosen + * interface or back to system interface selection. The `multicastInterface` must + * be a valid string representation of an IP from the socket's family. + * + * For IPv4 sockets, this should be the IP configured for the desired physical + * interface. All packets sent to multicast on the socket will be sent on the + * interface determined by the most recent successful use of this call. + * + * For IPv6 sockets, `multicastInterface` should include a scope to indicate the + * interface as in the examples that follow. In IPv6, individual `send` calls can + * also use explicit scope in addresses, so only packets sent to a multicast + * address without specifying an explicit scope are affected by the most recent + * successful use of this call. + * + * This method throws `EBADF` if called on an unbound socket. + * + * #### Example: IPv6 outgoing multicast interface + * + * On most systems, where scope format uses the interface name: + * + * ```js + * const socket = dgram.createSocket('udp6'); + * + * socket.bind(1234, () => { + * socket.setMulticastInterface('::%eth1'); + * }); + * ``` + * + * On Windows, where scope format uses an interface number: + * + * ```js + * const socket = dgram.createSocket('udp6'); + * + * socket.bind(1234, () => { + * socket.setMulticastInterface('::%2'); + * }); + * ``` + * + * #### Example: IPv4 outgoing multicast interface + * + * All systems use an IP of the host on the desired physical interface: + * + * ```js + * const socket = dgram.createSocket('udp4'); + * + * socket.bind(1234, () => { + * socket.setMulticastInterface('10.0.0.2'); + * }); + * ``` + * @since v8.6.0 + */ + setMulticastInterface(multicastInterface: string): void; + /** + * Sets or clears the `IP_MULTICAST_LOOP` socket option. When set to `true`, + * multicast packets will also be received on the local interface. + * + * This method throws `EBADF` if called on an unbound socket. + * @since v0.3.8 + */ + setMulticastLoopback(flag: boolean): boolean; + /** + * Sets the `IP_MULTICAST_TTL` socket option. While TTL generally stands for + * "Time to Live", in this context it specifies the number of IP hops that a + * packet is allowed to travel through, specifically for multicast traffic. Each + * router or gateway that forwards a packet decrements the TTL. If the TTL is + * decremented to 0 by a router, it will not be forwarded. + * + * The `ttl` argument may be between 0 and 255\. The default on most systems is `1`. + * + * This method throws `EBADF` if called on an unbound socket. + * @since v0.3.8 + */ + setMulticastTTL(ttl: number): number; + /** + * Sets the `SO_RCVBUF` socket option. Sets the maximum socket receive buffer + * in bytes. + * + * This method throws `ERR_SOCKET_BUFFER_SIZE` if called on an unbound socket. + * @since v8.7.0 + */ + setRecvBufferSize(size: number): void; + /** + * Sets the `SO_SNDBUF` socket option. Sets the maximum socket send buffer + * in bytes. + * + * This method throws `ERR_SOCKET_BUFFER_SIZE` if called on an unbound socket. + * @since v8.7.0 + */ + setSendBufferSize(size: number): void; + /** + * Sets the `IP_TTL` socket option. While TTL generally stands for "Time to Live", + * in this context it specifies the number of IP hops that a packet is allowed to + * travel through. Each router or gateway that forwards a packet decrements the + * TTL. If the TTL is decremented to 0 by a router, it will not be forwarded. + * Changing TTL values is typically done for network probes or when multicasting. + * + * The `ttl` argument may be between 1 and 255\. The default on most systems + * is 64. + * + * This method throws `EBADF` if called on an unbound socket. + * @since v0.1.101 + */ + setTTL(ttl: number): number; + /** + * By default, binding a socket will cause it to block the Node.js process from + * exiting as long as the socket is open. The `socket.unref()` method can be used + * to exclude the socket from the reference counting that keeps the Node.js + * process active, allowing the process to exit even if the socket is still + * listening. + * + * Calling `socket.unref()` multiple times will have no additional effect. + * + * The `socket.unref()` method returns a reference to the socket so calls can be + * chained. + * @since v0.9.1 + */ + unref(): this; + /** + * Tells the kernel to join a source-specific multicast channel at the given `sourceAddress` and `groupAddress`, using the `multicastInterface` with the `IP_ADD_SOURCE_MEMBERSHIP` socket + * option. If the `multicastInterface` argument + * is not specified, the operating system will choose one interface and will add + * membership to it. To add membership to every available interface, call `socket.addSourceSpecificMembership()` multiple times, once per interface. + * + * When called on an unbound socket, this method will implicitly bind to a random + * port, listening on all interfaces. + * @since v13.1.0, v12.16.0 + */ + addSourceSpecificMembership(sourceAddress: string, groupAddress: string, multicastInterface?: string): void; + /** + * Instructs the kernel to leave a source-specific multicast channel at the given `sourceAddress` and `groupAddress` using the `IP_DROP_SOURCE_MEMBERSHIP` socket option. This method is + * automatically called by the kernel when the + * socket is closed or the process terminates, so most apps will never have + * reason to call this. + * + * If `multicastInterface` is not specified, the operating system will attempt to + * drop membership on all valid interfaces. + * @since v13.1.0, v12.16.0 + */ + dropSourceSpecificMembership(sourceAddress: string, groupAddress: string, multicastInterface?: string): void; + /** + * events.EventEmitter + * 1. close + * 2. connect + * 3. error + * 4. listening + * 5. message + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "connect", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "listening", listener: () => void): this; + addListener(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "close"): boolean; + emit(event: "connect"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "listening"): boolean; + emit(event: "message", msg: NonSharedBuffer, rinfo: RemoteInfo): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "close", listener: () => void): this; + on(event: "connect", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "listening", listener: () => void): this; + on(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "connect", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "listening", listener: () => void): this; + once(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "connect", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "listening", listener: () => void): this; + prependListener(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "connect", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "listening", listener: () => void): this; + prependOnceListener(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this; + /** + * Calls `socket.close()` and returns a promise that fulfills when the socket has closed. + * @since v20.5.0 + */ + [Symbol.asyncDispose](): Promise; + } +} +declare module "node:dgram" { + export * from "dgram"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/diagnostics_channel.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/diagnostics_channel.d.ts new file mode 100644 index 00000000..f3bac527 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/diagnostics_channel.d.ts @@ -0,0 +1,578 @@ +/** + * The `node:diagnostics_channel` module provides an API to create named channels + * to report arbitrary message data for diagnostics purposes. + * + * It can be accessed using: + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * ``` + * + * It is intended that a module writer wanting to report diagnostics messages + * will create one or many top-level channels to report messages through. + * Channels may also be acquired at runtime but it is not encouraged + * due to the additional overhead of doing so. Channels may be exported for + * convenience, but as long as the name is known it can be acquired anywhere. + * + * If you intend for your module to produce diagnostics data for others to + * consume it is recommended that you include documentation of what named + * channels are used along with the shape of the message data. Channel names + * should generally include the module name to avoid collisions with data from + * other modules. + * @since v15.1.0, v14.17.0 + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/diagnostics_channel.js) + */ +declare module "diagnostics_channel" { + import { AsyncLocalStorage } from "node:async_hooks"; + /** + * Check if there are active subscribers to the named channel. This is helpful if + * the message you want to send might be expensive to prepare. + * + * This API is optional but helpful when trying to publish messages from very + * performance-sensitive code. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * if (diagnostics_channel.hasSubscribers('my-channel')) { + * // There are subscribers, prepare and publish message + * } + * ``` + * @since v15.1.0, v14.17.0 + * @param name The channel name + * @return If there are active subscribers + */ + function hasSubscribers(name: string | symbol): boolean; + /** + * This is the primary entry-point for anyone wanting to publish to a named + * channel. It produces a channel object which is optimized to reduce overhead at + * publish time as much as possible. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * ``` + * @since v15.1.0, v14.17.0 + * @param name The channel name + * @return The named channel object + */ + function channel(name: string | symbol): Channel; + type ChannelListener = (message: unknown, name: string | symbol) => void; + /** + * Register a message handler to subscribe to this channel. This message handler + * will be run synchronously whenever a message is published to the channel. Any + * errors thrown in the message handler will trigger an `'uncaughtException'`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * diagnostics_channel.subscribe('my-channel', (message, name) => { + * // Received data + * }); + * ``` + * @since v18.7.0, v16.17.0 + * @param name The channel name + * @param onMessage The handler to receive channel messages + */ + function subscribe(name: string | symbol, onMessage: ChannelListener): void; + /** + * Remove a message handler previously registered to this channel with {@link subscribe}. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * function onMessage(message, name) { + * // Received data + * } + * + * diagnostics_channel.subscribe('my-channel', onMessage); + * + * diagnostics_channel.unsubscribe('my-channel', onMessage); + * ``` + * @since v18.7.0, v16.17.0 + * @param name The channel name + * @param onMessage The previous subscribed handler to remove + * @return `true` if the handler was found, `false` otherwise. + */ + function unsubscribe(name: string | symbol, onMessage: ChannelListener): boolean; + /** + * Creates a `TracingChannel` wrapper for the given `TracingChannel Channels`. If a name is given, the corresponding tracing + * channels will be created in the form of `tracing:${name}:${eventType}` where `eventType` corresponds to the types of `TracingChannel Channels`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channelsByName = diagnostics_channel.tracingChannel('my-channel'); + * + * // or... + * + * const channelsByCollection = diagnostics_channel.tracingChannel({ + * start: diagnostics_channel.channel('tracing:my-channel:start'), + * end: diagnostics_channel.channel('tracing:my-channel:end'), + * asyncStart: diagnostics_channel.channel('tracing:my-channel:asyncStart'), + * asyncEnd: diagnostics_channel.channel('tracing:my-channel:asyncEnd'), + * error: diagnostics_channel.channel('tracing:my-channel:error'), + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param nameOrChannels Channel name or object containing all the `TracingChannel Channels` + * @return Collection of channels to trace with + */ + function tracingChannel< + StoreType = unknown, + ContextType extends object = StoreType extends object ? StoreType : object, + >( + nameOrChannels: string | TracingChannelCollection, + ): TracingChannel; + /** + * The class `Channel` represents an individual named channel within the data + * pipeline. It is used to track subscribers and to publish messages when there + * are subscribers present. It exists as a separate object to avoid channel + * lookups at publish time, enabling very fast publish speeds and allowing + * for heavy use while incurring very minimal cost. Channels are created with {@link channel}, constructing a channel directly + * with `new Channel(name)` is not supported. + * @since v15.1.0, v14.17.0 + */ + class Channel { + readonly name: string | symbol; + /** + * Check if there are active subscribers to this channel. This is helpful if + * the message you want to send might be expensive to prepare. + * + * This API is optional but helpful when trying to publish messages from very + * performance-sensitive code. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * if (channel.hasSubscribers) { + * // There are subscribers, prepare and publish message + * } + * ``` + * @since v15.1.0, v14.17.0 + */ + readonly hasSubscribers: boolean; + private constructor(name: string | symbol); + /** + * Publish a message to any subscribers to the channel. This will trigger + * message handlers synchronously so they will execute within the same context. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.publish({ + * some: 'message', + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @param message The message to send to the channel subscribers + */ + publish(message: unknown): void; + /** + * Register a message handler to subscribe to this channel. This message handler + * will be run synchronously whenever a message is published to the channel. Any + * errors thrown in the message handler will trigger an `'uncaughtException'`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.subscribe((message, name) => { + * // Received data + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @deprecated Since v18.7.0,v16.17.0 - Use {@link subscribe(name, onMessage)} + * @param onMessage The handler to receive channel messages + */ + subscribe(onMessage: ChannelListener): void; + /** + * Remove a message handler previously registered to this channel with `channel.subscribe(onMessage)`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * function onMessage(message, name) { + * // Received data + * } + * + * channel.subscribe(onMessage); + * + * channel.unsubscribe(onMessage); + * ``` + * @since v15.1.0, v14.17.0 + * @deprecated Since v18.7.0,v16.17.0 - Use {@link unsubscribe(name, onMessage)} + * @param onMessage The previous subscribed handler to remove + * @return `true` if the handler was found, `false` otherwise. + */ + unsubscribe(onMessage: ChannelListener): void; + /** + * When `channel.runStores(context, ...)` is called, the given context data + * will be applied to any store bound to the channel. If the store has already been + * bound the previous `transform` function will be replaced with the new one. + * The `transform` function may be omitted to set the given context data as the + * context directly. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const store = new AsyncLocalStorage(); + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.bindStore(store, (data) => { + * return { data }; + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param store The store to which to bind the context data + * @param transform Transform context data before setting the store context + */ + bindStore(store: AsyncLocalStorage, transform?: (context: ContextType) => StoreType): void; + /** + * Remove a message handler previously registered to this channel with `channel.bindStore(store)`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const store = new AsyncLocalStorage(); + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.bindStore(store); + * channel.unbindStore(store); + * ``` + * @since v19.9.0 + * @experimental + * @param store The store to unbind from the channel. + * @return `true` if the store was found, `false` otherwise. + */ + unbindStore(store: AsyncLocalStorage): boolean; + /** + * Applies the given data to any AsyncLocalStorage instances bound to the channel + * for the duration of the given function, then publishes to the channel within + * the scope of that data is applied to the stores. + * + * If a transform function was given to `channel.bindStore(store)` it will be + * applied to transform the message data before it becomes the context value for + * the store. The prior storage context is accessible from within the transform + * function in cases where context linking is required. + * + * The context applied to the store should be accessible in any async code which + * continues from execution which began during the given function, however + * there are some situations in which `context loss` may occur. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const store = new AsyncLocalStorage(); + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.bindStore(store, (message) => { + * const parent = store.getStore(); + * return new Span(message, parent); + * }); + * channel.runStores({ some: 'message' }, () => { + * store.getStore(); // Span({ some: 'message' }) + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param context Message to send to subscribers and bind to stores + * @param fn Handler to run within the entered storage context + * @param thisArg The receiver to be used for the function call. + * @param args Optional arguments to pass to the function. + */ + runStores( + context: ContextType, + fn: (this: ThisArg, ...args: Args) => Result, + thisArg?: ThisArg, + ...args: Args + ): Result; + } + interface TracingChannelSubscribers { + start: (message: ContextType) => void; + end: ( + message: ContextType & { + error?: unknown; + result?: unknown; + }, + ) => void; + asyncStart: ( + message: ContextType & { + error?: unknown; + result?: unknown; + }, + ) => void; + asyncEnd: ( + message: ContextType & { + error?: unknown; + result?: unknown; + }, + ) => void; + error: ( + message: ContextType & { + error: unknown; + }, + ) => void; + } + interface TracingChannelCollection { + start: Channel; + end: Channel; + asyncStart: Channel; + asyncEnd: Channel; + error: Channel; + } + /** + * The class `TracingChannel` is a collection of `TracingChannel Channels` which + * together express a single traceable action. It is used to formalize and + * simplify the process of producing events for tracing application flow. {@link tracingChannel} is used to construct a `TracingChannel`. As with `Channel` it is recommended to create and reuse a + * single `TracingChannel` at the top-level of the file rather than creating them + * dynamically. + * @since v19.9.0 + * @experimental + */ + class TracingChannel implements TracingChannelCollection { + start: Channel; + end: Channel; + asyncStart: Channel; + asyncEnd: Channel; + error: Channel; + /** + * Helper to subscribe a collection of functions to the corresponding channels. + * This is the same as calling `channel.subscribe(onMessage)` on each channel + * individually. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.subscribe({ + * start(message) { + * // Handle start message + * }, + * end(message) { + * // Handle end message + * }, + * asyncStart(message) { + * // Handle asyncStart message + * }, + * asyncEnd(message) { + * // Handle asyncEnd message + * }, + * error(message) { + * // Handle error message + * }, + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param subscribers Set of `TracingChannel Channels` subscribers + */ + subscribe(subscribers: TracingChannelSubscribers): void; + /** + * Helper to unsubscribe a collection of functions from the corresponding channels. + * This is the same as calling `channel.unsubscribe(onMessage)` on each channel + * individually. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.unsubscribe({ + * start(message) { + * // Handle start message + * }, + * end(message) { + * // Handle end message + * }, + * asyncStart(message) { + * // Handle asyncStart message + * }, + * asyncEnd(message) { + * // Handle asyncEnd message + * }, + * error(message) { + * // Handle error message + * }, + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param subscribers Set of `TracingChannel Channels` subscribers + * @return `true` if all handlers were successfully unsubscribed, and `false` otherwise. + */ + unsubscribe(subscribers: TracingChannelSubscribers): void; + /** + * Trace a synchronous function call. This will always produce a `start event` and `end event` around the execution and may produce an `error event` if the given function throws an error. + * This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all + * events should have any bound stores set to match this trace context. + * + * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions + * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.traceSync(() => { + * // Do something + * }, { + * some: 'thing', + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param fn Function to wrap a trace around + * @param context Shared object to correlate events through + * @param thisArg The receiver to be used for the function call + * @param args Optional arguments to pass to the function + * @return The return value of the given function + */ + traceSync( + fn: (this: ThisArg, ...args: Args) => Result, + context?: ContextType, + thisArg?: ThisArg, + ...args: Args + ): Result; + /** + * Trace a promise-returning function call. This will always produce a `start event` and `end event` around the synchronous portion of the + * function execution, and will produce an `asyncStart event` and `asyncEnd event` when a promise continuation is reached. It may also + * produce an `error event` if the given function throws an error or the + * returned promise rejects. This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all + * events should have any bound stores set to match this trace context. + * + * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions + * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.tracePromise(async () => { + * // Do something + * }, { + * some: 'thing', + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param fn Promise-returning function to wrap a trace around + * @param context Shared object to correlate trace events through + * @param thisArg The receiver to be used for the function call + * @param args Optional arguments to pass to the function + * @return Chained from promise returned by the given function + */ + tracePromise( + fn: (this: ThisArg, ...args: Args) => Promise, + context?: ContextType, + thisArg?: ThisArg, + ...args: Args + ): Promise; + /** + * Trace a callback-receiving function call. This will always produce a `start event` and `end event` around the synchronous portion of the + * function execution, and will produce a `asyncStart event` and `asyncEnd event` around the callback execution. It may also produce an `error event` if the given function throws an error or + * the returned + * promise rejects. This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all + * events should have any bound stores set to match this trace context. + * + * The `position` will be -1 by default to indicate the final argument should + * be used as the callback. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.traceCallback((arg1, callback) => { + * // Do something + * callback(null, 'result'); + * }, 1, { + * some: 'thing', + * }, thisArg, arg1, callback); + * ``` + * + * The callback will also be run with `channel.runStores(context, ...)` which + * enables context loss recovery in some cases. + * + * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions + * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * const myStore = new AsyncLocalStorage(); + * + * // The start channel sets the initial store data to something + * // and stores that store data value on the trace context object + * channels.start.bindStore(myStore, (data) => { + * const span = new Span(data); + * data.span = span; + * return span; + * }); + * + * // Then asyncStart can restore from that data it stored previously + * channels.asyncStart.bindStore(myStore, (data) => { + * return data.span; + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param fn callback using function to wrap a trace around + * @param position Zero-indexed argument position of expected callback + * @param context Shared object to correlate trace events through + * @param thisArg The receiver to be used for the function call + * @param args Optional arguments to pass to the function + * @return The return value of the given function + */ + traceCallback( + fn: (this: ThisArg, ...args: Args) => Result, + position?: number, + context?: ContextType, + thisArg?: ThisArg, + ...args: Args + ): Result; + /** + * `true` if any of the individual channels has a subscriber, `false` if not. + * + * This is a helper method available on a {@link TracingChannel} instance to check + * if any of the [TracingChannel Channels](https://nodejs.org/api/diagnostics_channel.html#tracingchannel-channels) have subscribers. + * A `true` is returned if any of them have at least one subscriber, a `false` is returned otherwise. + * + * ```js + * const diagnostics_channel = require('node:diagnostics_channel'); + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * if (channels.hasSubscribers) { + * // Do something + * } + * ``` + * @since v22.0.0, v20.13.0 + */ + readonly hasSubscribers: boolean; + } +} +declare module "node:diagnostics_channel" { + export * from "diagnostics_channel"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dns.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dns.d.ts new file mode 100644 index 00000000..9cb20559 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dns.d.ts @@ -0,0 +1,923 @@ +/** + * The `node:dns` module enables name resolution. For example, use it to look up IP + * addresses of host names. + * + * Although named for the [Domain Name System (DNS)](https://en.wikipedia.org/wiki/Domain_Name_System), it does not always use the + * DNS protocol for lookups. {@link lookup} uses the operating system + * facilities to perform name resolution. It may not need to perform any network + * communication. To perform name resolution the way other applications on the same + * system do, use {@link lookup}. + * + * ```js + * import dns from 'node:dns'; + * + * dns.lookup('example.org', (err, address, family) => { + * console.log('address: %j family: IPv%s', address, family); + * }); + * // address: "93.184.216.34" family: IPv4 + * ``` + * + * All other functions in the `node:dns` module connect to an actual DNS server to + * perform name resolution. They will always use the network to perform DNS + * queries. These functions do not use the same set of configuration files used by {@link lookup} (e.g. `/etc/hosts`). Use these functions to always perform + * DNS queries, bypassing other name-resolution facilities. + * + * ```js + * import dns from 'node:dns'; + * + * dns.resolve4('archive.org', (err, addresses) => { + * if (err) throw err; + * + * console.log(`addresses: ${JSON.stringify(addresses)}`); + * + * addresses.forEach((a) => { + * dns.reverse(a, (err, hostnames) => { + * if (err) { + * throw err; + * } + * console.log(`reverse for ${a}: ${JSON.stringify(hostnames)}`); + * }); + * }); + * }); + * ``` + * + * See the [Implementation considerations section](https://nodejs.org/docs/latest-v22.x/api/dns.html#implementation-considerations) for more information. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/dns.js) + */ +declare module "dns" { + import * as dnsPromises from "node:dns/promises"; + // Supported getaddrinfo flags. + /** + * Limits returned address types to the types of non-loopback addresses configured on the system. For example, IPv4 addresses are + * only returned if the current system has at least one IPv4 address configured. + */ + export const ADDRCONFIG: number; + /** + * If the IPv6 family was specified, but no IPv6 addresses were found, then return IPv4 mapped IPv6 addresses. It is not supported + * on some operating systems (e.g. FreeBSD 10.1). + */ + export const V4MAPPED: number; + /** + * If `dns.V4MAPPED` is specified, return resolved IPv6 addresses as + * well as IPv4 mapped IPv6 addresses. + */ + export const ALL: number; + export interface LookupOptions { + /** + * The record family. Must be `4`, `6`, or `0`. For backward compatibility reasons, `'IPv4'` and `'IPv6'` are interpreted + * as `4` and `6` respectively. The value 0 indicates that either an IPv4 or IPv6 address is returned. If the value `0` is used + * with `{ all: true } (see below)`, both IPv4 and IPv6 addresses are returned. + * @default 0 + */ + family?: number | "IPv4" | "IPv6" | undefined; + /** + * One or more [supported `getaddrinfo`](https://nodejs.org/docs/latest-v22.x/api/dns.html#supported-getaddrinfo-flags) flags. Multiple flags may be + * passed by bitwise `OR`ing their values. + */ + hints?: number | undefined; + /** + * When `true`, the callback returns all resolved addresses in an array. Otherwise, returns a single address. + * @default false + */ + all?: boolean | undefined; + /** + * When `verbatim`, the resolved addresses are return unsorted. When `ipv4first`, the resolved addresses are sorted + * by placing IPv4 addresses before IPv6 addresses. When `ipv6first`, the resolved addresses are sorted by placing IPv6 + * addresses before IPv4 addresses. Default value is configurable using + * {@link setDefaultResultOrder} or [`--dns-result-order`](https://nodejs.org/docs/latest-v22.x/api/cli.html#--dns-result-orderorder). + * @default `verbatim` (addresses are not reordered) + * @since v22.1.0 + */ + order?: "ipv4first" | "ipv6first" | "verbatim" | undefined; + /** + * When `true`, the callback receives IPv4 and IPv6 addresses in the order the DNS resolver returned them. When `false`, IPv4 + * addresses are placed before IPv6 addresses. This option will be deprecated in favor of `order`. When both are specified, + * `order` has higher precedence. New code should only use `order`. Default value is configurable using {@link setDefaultResultOrder} + * @default true (addresses are not reordered) + * @deprecated Please use `order` option + */ + verbatim?: boolean | undefined; + } + export interface LookupOneOptions extends LookupOptions { + all?: false | undefined; + } + export interface LookupAllOptions extends LookupOptions { + all: true; + } + export interface LookupAddress { + /** + * A string representation of an IPv4 or IPv6 address. + */ + address: string; + /** + * `4` or `6`, denoting the family of `address`, or `0` if the address is not an IPv4 or IPv6 address. `0` is a likely indicator of a + * bug in the name resolution service used by the operating system. + */ + family: number; + } + /** + * Resolves a host name (e.g. `'nodejs.org'`) into the first found A (IPv4) or + * AAAA (IPv6) record. All `option` properties are optional. If `options` is an + * integer, then it must be `4` or `6` – if `options` is `0` or not provided, then + * IPv4 and IPv6 addresses are both returned if found. + * + * With the `all` option set to `true`, the arguments for `callback` change to `(err, addresses)`, with `addresses` being an array of objects with the + * properties `address` and `family`. + * + * On error, `err` is an `Error` object, where `err.code` is the error code. + * Keep in mind that `err.code` will be set to `'ENOTFOUND'` not only when + * the host name does not exist but also when the lookup fails in other ways + * such as no available file descriptors. + * + * `dns.lookup()` does not necessarily have anything to do with the DNS protocol. + * The implementation uses an operating system facility that can associate names + * with addresses and vice versa. This implementation can have subtle but + * important consequences on the behavior of any Node.js program. Please take some + * time to consult the [Implementation considerations section](https://nodejs.org/docs/latest-v22.x/api/dns.html#implementation-considerations) + * before using `dns.lookup()`. + * + * Example usage: + * + * ```js + * import dns from 'node:dns'; + * const options = { + * family: 6, + * hints: dns.ADDRCONFIG | dns.V4MAPPED, + * }; + * dns.lookup('example.com', options, (err, address, family) => + * console.log('address: %j family: IPv%s', address, family)); + * // address: "2606:2800:220:1:248:1893:25c8:1946" family: IPv6 + * + * // When options.all is true, the result will be an Array. + * options.all = true; + * dns.lookup('example.com', options, (err, addresses) => + * console.log('addresses: %j', addresses)); + * // addresses: [{"address":"2606:2800:220:1:248:1893:25c8:1946","family":6}] + * ``` + * + * If this method is invoked as its [util.promisify()](https://nodejs.org/docs/latest-v22.x/api/util.html#utilpromisifyoriginal) ed + * version, and `all` is not set to `true`, it returns a `Promise` for an `Object` with `address` and `family` properties. + * @since v0.1.90 + */ + export function lookup( + hostname: string, + family: number, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export function lookup( + hostname: string, + options: LookupOneOptions, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export function lookup( + hostname: string, + options: LookupAllOptions, + callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void, + ): void; + export function lookup( + hostname: string, + options: LookupOptions, + callback: (err: NodeJS.ErrnoException | null, address: string | LookupAddress[], family: number) => void, + ): void; + export function lookup( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export namespace lookup { + function __promisify__(hostname: string, options: LookupAllOptions): Promise; + function __promisify__(hostname: string, options?: LookupOneOptions | number): Promise; + function __promisify__(hostname: string, options: LookupOptions): Promise; + } + /** + * Resolves the given `address` and `port` into a host name and service using + * the operating system's underlying `getnameinfo` implementation. + * + * If `address` is not a valid IP address, a `TypeError` will be thrown. + * The `port` will be coerced to a number. If it is not a legal port, a `TypeError` will be thrown. + * + * On an error, `err` is an [`Error`](https://nodejs.org/docs/latest-v22.x/api/errors.html#class-error) object, + * where `err.code` is the error code. + * + * ```js + * import dns from 'node:dns'; + * dns.lookupService('127.0.0.1', 22, (err, hostname, service) => { + * console.log(hostname, service); + * // Prints: localhost ssh + * }); + * ``` + * + * If this method is invoked as its [util.promisify()](https://nodejs.org/docs/latest-v22.x/api/util.html#utilpromisifyoriginal) ed + * version, it returns a `Promise` for an `Object` with `hostname` and `service` properties. + * @since v0.11.14 + */ + export function lookupService( + address: string, + port: number, + callback: (err: NodeJS.ErrnoException | null, hostname: string, service: string) => void, + ): void; + export namespace lookupService { + function __promisify__( + address: string, + port: number, + ): Promise<{ + hostname: string; + service: string; + }>; + } + export interface ResolveOptions { + ttl: boolean; + } + export interface ResolveWithTtlOptions extends ResolveOptions { + ttl: true; + } + export interface RecordWithTtl { + address: string; + ttl: number; + } + /** @deprecated Use `AnyARecord` or `AnyAaaaRecord` instead. */ + export type AnyRecordWithTtl = AnyARecord | AnyAaaaRecord; + export interface AnyARecord extends RecordWithTtl { + type: "A"; + } + export interface AnyAaaaRecord extends RecordWithTtl { + type: "AAAA"; + } + export interface CaaRecord { + critical: number; + issue?: string | undefined; + issuewild?: string | undefined; + iodef?: string | undefined; + contactemail?: string | undefined; + contactphone?: string | undefined; + } + export interface AnyCaaRecord extends CaaRecord { + type: "CAA"; + } + export interface MxRecord { + priority: number; + exchange: string; + } + export interface AnyMxRecord extends MxRecord { + type: "MX"; + } + export interface NaptrRecord { + flags: string; + service: string; + regexp: string; + replacement: string; + order: number; + preference: number; + } + export interface AnyNaptrRecord extends NaptrRecord { + type: "NAPTR"; + } + export interface SoaRecord { + nsname: string; + hostmaster: string; + serial: number; + refresh: number; + retry: number; + expire: number; + minttl: number; + } + export interface AnySoaRecord extends SoaRecord { + type: "SOA"; + } + export interface SrvRecord { + priority: number; + weight: number; + port: number; + name: string; + } + export interface AnySrvRecord extends SrvRecord { + type: "SRV"; + } + export interface TlsaRecord { + certUsage: number; + selector: number; + match: number; + data: ArrayBuffer; + } + export interface AnyTlsaRecord extends TlsaRecord { + type: "TLSA"; + } + export interface AnyTxtRecord { + type: "TXT"; + entries: string[]; + } + export interface AnyNsRecord { + type: "NS"; + value: string; + } + export interface AnyPtrRecord { + type: "PTR"; + value: string; + } + export interface AnyCnameRecord { + type: "CNAME"; + value: string; + } + export type AnyRecord = + | AnyARecord + | AnyAaaaRecord + | AnyCaaRecord + | AnyCnameRecord + | AnyMxRecord + | AnyNaptrRecord + | AnyNsRecord + | AnyPtrRecord + | AnySoaRecord + | AnySrvRecord + | AnyTlsaRecord + | AnyTxtRecord; + /** + * Uses the DNS protocol to resolve a host name (e.g. `'nodejs.org'`) into an array + * of the resource records. The `callback` function has arguments `(err, records)`. When successful, `records` will be an array of resource + * records. The type and structure of individual results varies based on `rrtype`: + * + * + * + * On error, `err` is an [`Error`](https://nodejs.org/docs/latest-v22.x/api/errors.html#class-error) object, + * where `err.code` is one of the `DNS error codes`. + * @since v0.1.27 + * @param hostname Host name to resolve. + * @param [rrtype='A'] Resource record type. + */ + export function resolve( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "A" | "AAAA" | "CNAME" | "NS" | "PTR", + callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "ANY", + callback: (err: NodeJS.ErrnoException | null, addresses: AnyRecord[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "CAA", + callback: (err: NodeJS.ErrnoException | null, address: CaaRecord[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "MX", + callback: (err: NodeJS.ErrnoException | null, addresses: MxRecord[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "NAPTR", + callback: (err: NodeJS.ErrnoException | null, addresses: NaptrRecord[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "SOA", + callback: (err: NodeJS.ErrnoException | null, addresses: SoaRecord) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "SRV", + callback: (err: NodeJS.ErrnoException | null, addresses: SrvRecord[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "TLSA", + callback: (err: NodeJS.ErrnoException | null, addresses: TlsaRecord[]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: "TXT", + callback: (err: NodeJS.ErrnoException | null, addresses: string[][]) => void, + ): void; + export function resolve( + hostname: string, + rrtype: string, + callback: ( + err: NodeJS.ErrnoException | null, + addresses: + | string[] + | CaaRecord[] + | MxRecord[] + | NaptrRecord[] + | SoaRecord + | SrvRecord[] + | TlsaRecord[] + | string[][] + | AnyRecord[], + ) => void, + ): void; + export namespace resolve { + function __promisify__(hostname: string, rrtype?: "A" | "AAAA" | "CNAME" | "NS" | "PTR"): Promise; + function __promisify__(hostname: string, rrtype: "ANY"): Promise; + function __promisify__(hostname: string, rrtype: "CAA"): Promise; + function __promisify__(hostname: string, rrtype: "MX"): Promise; + function __promisify__(hostname: string, rrtype: "NAPTR"): Promise; + function __promisify__(hostname: string, rrtype: "SOA"): Promise; + function __promisify__(hostname: string, rrtype: "SRV"): Promise; + function __promisify__(hostname: string, rrtype: "TLSA"): Promise; + function __promisify__(hostname: string, rrtype: "TXT"): Promise; + function __promisify__( + hostname: string, + rrtype: string, + ): Promise< + | string[] + | CaaRecord[] + | MxRecord[] + | NaptrRecord[] + | SoaRecord + | SrvRecord[] + | TlsaRecord[] + | string[][] + | AnyRecord[] + >; + } + /** + * Uses the DNS protocol to resolve a IPv4 addresses (`A` records) for the `hostname`. The `addresses` argument passed to the `callback` function + * will contain an array of IPv4 addresses (e.g.`['74.125.79.104', '74.125.79.105', '74.125.79.106']`). + * @since v0.1.16 + * @param hostname Host name to resolve. + */ + export function resolve4( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void, + ): void; + export function resolve4( + hostname: string, + options: ResolveWithTtlOptions, + callback: (err: NodeJS.ErrnoException | null, addresses: RecordWithTtl[]) => void, + ): void; + export function resolve4( + hostname: string, + options: ResolveOptions, + callback: (err: NodeJS.ErrnoException | null, addresses: string[] | RecordWithTtl[]) => void, + ): void; + export namespace resolve4 { + function __promisify__(hostname: string): Promise; + function __promisify__(hostname: string, options: ResolveWithTtlOptions): Promise; + function __promisify__(hostname: string, options?: ResolveOptions): Promise; + } + /** + * Uses the DNS protocol to resolve IPv6 addresses (`AAAA` records) for the `hostname`. The `addresses` argument passed to the `callback` function + * will contain an array of IPv6 addresses. + * @since v0.1.16 + * @param hostname Host name to resolve. + */ + export function resolve6( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void, + ): void; + export function resolve6( + hostname: string, + options: ResolveWithTtlOptions, + callback: (err: NodeJS.ErrnoException | null, addresses: RecordWithTtl[]) => void, + ): void; + export function resolve6( + hostname: string, + options: ResolveOptions, + callback: (err: NodeJS.ErrnoException | null, addresses: string[] | RecordWithTtl[]) => void, + ): void; + export namespace resolve6 { + function __promisify__(hostname: string): Promise; + function __promisify__(hostname: string, options: ResolveWithTtlOptions): Promise; + function __promisify__(hostname: string, options?: ResolveOptions): Promise; + } + /** + * Uses the DNS protocol to resolve `CNAME` records for the `hostname`. The `addresses` argument passed to the `callback` function + * will contain an array of canonical name records available for the `hostname` (e.g. `['bar.example.com']`). + * @since v0.3.2 + */ + export function resolveCname( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void, + ): void; + export namespace resolveCname { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve `CAA` records for the `hostname`. The `addresses` argument passed to the `callback` function + * will contain an array of certification authority authorization records + * available for the `hostname` (e.g. `[{critical: 0, iodef: 'mailto:pki@example.com'}, {critical: 128, issue: 'pki.example.com'}]`). + * @since v15.0.0, v14.17.0 + */ + export function resolveCaa( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, records: CaaRecord[]) => void, + ): void; + export namespace resolveCaa { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve mail exchange records (`MX` records) for the `hostname`. The `addresses` argument passed to the `callback` function will + * contain an array of objects containing both a `priority` and `exchange` property (e.g. `[{priority: 10, exchange: 'mx.example.com'}, ...]`). + * @since v0.1.27 + */ + export function resolveMx( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: MxRecord[]) => void, + ): void; + export namespace resolveMx { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve regular expression-based records (`NAPTR` records) for the `hostname`. The `addresses` argument passed to the `callback` function will contain an array of + * objects with the following properties: + * + * * `flags` + * * `service` + * * `regexp` + * * `replacement` + * * `order` + * * `preference` + * + * ```js + * { + * flags: 's', + * service: 'SIP+D2U', + * regexp: '', + * replacement: '_sip._udp.example.com', + * order: 30, + * preference: 100 + * } + * ``` + * @since v0.9.12 + */ + export function resolveNaptr( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: NaptrRecord[]) => void, + ): void; + export namespace resolveNaptr { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve name server records (`NS` records) for the `hostname`. The `addresses` argument passed to the `callback` function will + * contain an array of name server records available for `hostname` (e.g. `['ns1.example.com', 'ns2.example.com']`). + * @since v0.1.90 + */ + export function resolveNs( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void, + ): void; + export namespace resolveNs { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve pointer records (`PTR` records) for the `hostname`. The `addresses` argument passed to the `callback` function will + * be an array of strings containing the reply records. + * @since v6.0.0 + */ + export function resolvePtr( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void, + ): void; + export namespace resolvePtr { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve a start of authority record (`SOA` record) for + * the `hostname`. The `address` argument passed to the `callback` function will + * be an object with the following properties: + * + * * `nsname` + * * `hostmaster` + * * `serial` + * * `refresh` + * * `retry` + * * `expire` + * * `minttl` + * + * ```js + * { + * nsname: 'ns.example.com', + * hostmaster: 'root.example.com', + * serial: 2013101809, + * refresh: 10000, + * retry: 2400, + * expire: 604800, + * minttl: 3600 + * } + * ``` + * @since v0.11.10 + */ + export function resolveSoa( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, address: SoaRecord) => void, + ): void; + export namespace resolveSoa { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve service records (`SRV` records) for the `hostname`. The `addresses` argument passed to the `callback` function will + * be an array of objects with the following properties: + * + * * `priority` + * * `weight` + * * `port` + * * `name` + * + * ```js + * { + * priority: 10, + * weight: 5, + * port: 21223, + * name: 'service.example.com' + * } + * ``` + * @since v0.1.27 + */ + export function resolveSrv( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: SrvRecord[]) => void, + ): void; + export namespace resolveSrv { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve certificate associations (`TLSA` records) for + * the `hostname`. The `records` argument passed to the `callback` function is an + * array of objects with these properties: + * + * * `certUsage` + * * `selector` + * * `match` + * * `data` + * + * ```js + * { + * certUsage: 3, + * selector: 1, + * match: 1, + * data: [ArrayBuffer] + * } + * ``` + * @since v22.15.0 + */ + export function resolveTlsa( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: TlsaRecord[]) => void, + ): void; + export namespace resolveTlsa { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve text queries (`TXT` records) for the `hostname`. The `records` argument passed to the `callback` function is a + * two-dimensional array of the text records available for `hostname` (e.g.`[ ['v=spf1 ip4:0.0.0.0 ', '~all' ] ]`). Each sub-array contains TXT chunks of + * one record. Depending on the use case, these could be either joined together or + * treated separately. + * @since v0.1.27 + */ + export function resolveTxt( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: string[][]) => void, + ): void; + export namespace resolveTxt { + function __promisify__(hostname: string): Promise; + } + /** + * Uses the DNS protocol to resolve all records (also known as `ANY` or `*` query). + * The `ret` argument passed to the `callback` function will be an array containing + * various types of records. Each object has a property `type` that indicates the + * type of the current record. And depending on the `type`, additional properties + * will be present on the object: + * + * + * + * Here is an example of the `ret` object passed to the callback: + * + * ```js + * [ { type: 'A', address: '127.0.0.1', ttl: 299 }, + * { type: 'CNAME', value: 'example.com' }, + * { type: 'MX', exchange: 'alt4.aspmx.l.example.com', priority: 50 }, + * { type: 'NS', value: 'ns1.example.com' }, + * { type: 'TXT', entries: [ 'v=spf1 include:_spf.example.com ~all' ] }, + * { type: 'SOA', + * nsname: 'ns1.example.com', + * hostmaster: 'admin.example.com', + * serial: 156696742, + * refresh: 900, + * retry: 900, + * expire: 1800, + * minttl: 60 } ] + * ``` + * + * DNS server operators may choose not to respond to `ANY` queries. It may be better to call individual methods like {@link resolve4}, {@link resolveMx}, and so on. For more details, see + * [RFC 8482](https://tools.ietf.org/html/rfc8482). + */ + export function resolveAny( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, addresses: AnyRecord[]) => void, + ): void; + export namespace resolveAny { + function __promisify__(hostname: string): Promise; + } + /** + * Performs a reverse DNS query that resolves an IPv4 or IPv6 address to an + * array of host names. + * + * On error, `err` is an [`Error`](https://nodejs.org/docs/latest-v22.x/api/errors.html#class-error) object, where `err.code` is + * one of the [DNS error codes](https://nodejs.org/docs/latest-v22.x/api/dns.html#error-codes). + * @since v0.1.16 + */ + export function reverse( + ip: string, + callback: (err: NodeJS.ErrnoException | null, hostnames: string[]) => void, + ): void; + /** + * Get the default value for `order` in {@link lookup} and [`dnsPromises.lookup()`](https://nodejs.org/docs/latest-v22.x/api/dns.html#dnspromiseslookuphostname-options). + * The value could be: + * + * * `ipv4first`: for `order` defaulting to `ipv4first`. + * * `ipv6first`: for `order` defaulting to `ipv6first`. + * * `verbatim`: for `order` defaulting to `verbatim`. + * @since v18.17.0 + */ + export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim"; + /** + * Sets the IP address and port of servers to be used when performing DNS + * resolution. The `servers` argument is an array of [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6) formatted + * addresses. If the port is the IANA default DNS port (53) it can be omitted. + * + * ```js + * dns.setServers([ + * '4.4.4.4', + * '[2001:4860:4860::8888]', + * '4.4.4.4:1053', + * '[2001:4860:4860::8888]:1053', + * ]); + * ``` + * + * An error will be thrown if an invalid address is provided. + * + * The `dns.setServers()` method must not be called while a DNS query is in + * progress. + * + * The {@link setServers} method affects only {@link resolve}, `dns.resolve*()` and {@link reverse} (and specifically _not_ {@link lookup}). + * + * This method works much like [resolve.conf](https://man7.org/linux/man-pages/man5/resolv.conf.5.html). + * That is, if attempting to resolve with the first server provided results in a `NOTFOUND` error, the `resolve()` method will _not_ attempt to resolve with + * subsequent servers provided. Fallback DNS servers will only be used if the + * earlier ones time out or result in some other error. + * @since v0.11.3 + * @param servers array of [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952#section-6) formatted addresses + */ + export function setServers(servers: readonly string[]): void; + /** + * Returns an array of IP address strings, formatted according to [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6), + * that are currently configured for DNS resolution. A string will include a port + * section if a custom port is used. + * + * ```js + * [ + * '4.4.4.4', + * '2001:4860:4860::8888', + * '4.4.4.4:1053', + * '[2001:4860:4860::8888]:1053', + * ] + * ``` + * @since v0.11.3 + */ + export function getServers(): string[]; + /** + * Set the default value of `order` in {@link lookup} and [`dnsPromises.lookup()`](https://nodejs.org/docs/latest-v22.x/api/dns.html#dnspromiseslookuphostname-options). + * The value could be: + * + * * `ipv4first`: sets default `order` to `ipv4first`. + * * `ipv6first`: sets default `order` to `ipv6first`. + * * `verbatim`: sets default `order` to `verbatim`. + * + * The default is `verbatim` and {@link setDefaultResultOrder} have higher + * priority than [`--dns-result-order`](https://nodejs.org/docs/latest-v22.x/api/cli.html#--dns-result-orderorder). When using + * [worker threads](https://nodejs.org/docs/latest-v22.x/api/worker_threads.html), {@link setDefaultResultOrder} from the main + * thread won't affect the default dns orders in workers. + * @since v16.4.0, v14.18.0 + * @param order must be `'ipv4first'`, `'ipv6first'` or `'verbatim'`. + */ + export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void; + // Error codes + export const NODATA: "ENODATA"; + export const FORMERR: "EFORMERR"; + export const SERVFAIL: "ESERVFAIL"; + export const NOTFOUND: "ENOTFOUND"; + export const NOTIMP: "ENOTIMP"; + export const REFUSED: "EREFUSED"; + export const BADQUERY: "EBADQUERY"; + export const BADNAME: "EBADNAME"; + export const BADFAMILY: "EBADFAMILY"; + export const BADRESP: "EBADRESP"; + export const CONNREFUSED: "ECONNREFUSED"; + export const TIMEOUT: "ETIMEOUT"; + export const EOF: "EOF"; + export const FILE: "EFILE"; + export const NOMEM: "ENOMEM"; + export const DESTRUCTION: "EDESTRUCTION"; + export const BADSTR: "EBADSTR"; + export const BADFLAGS: "EBADFLAGS"; + export const NONAME: "ENONAME"; + export const BADHINTS: "EBADHINTS"; + export const NOTINITIALIZED: "ENOTINITIALIZED"; + export const LOADIPHLPAPI: "ELOADIPHLPAPI"; + export const ADDRGETNETWORKPARAMS: "EADDRGETNETWORKPARAMS"; + export const CANCELLED: "ECANCELLED"; + export interface ResolverOptions { + /** + * Query timeout in milliseconds, or `-1` to use the default timeout. + */ + timeout?: number | undefined; + /** + * The number of tries the resolver will try contacting each name server before giving up. + * @default 4 + */ + tries?: number | undefined; + /** + * The max retry timeout, in milliseconds. + * @default 0 + */ + maxTimeout?: number | undefined; + } + /** + * An independent resolver for DNS requests. + * + * Creating a new resolver uses the default server settings. Setting + * the servers used for a resolver using [`resolver.setServers()`](https://nodejs.org/docs/latest-v22.x/api/dns.html#dnssetserversservers) does not affect + * other resolvers: + * + * ```js + * import { Resolver } from 'node:dns'; + * const resolver = new Resolver(); + * resolver.setServers(['4.4.4.4']); + * + * // This request will use the server at 4.4.4.4, independent of global settings. + * resolver.resolve4('example.org', (err, addresses) => { + * // ... + * }); + * ``` + * + * The following methods from the `node:dns` module are available: + * + * * `resolver.getServers()` + * * `resolver.resolve()` + * * `resolver.resolve4()` + * * `resolver.resolve6()` + * * `resolver.resolveAny()` + * * `resolver.resolveCaa()` + * * `resolver.resolveCname()` + * * `resolver.resolveMx()` + * * `resolver.resolveNaptr()` + * * `resolver.resolveNs()` + * * `resolver.resolvePtr()` + * * `resolver.resolveSoa()` + * * `resolver.resolveSrv()` + * * `resolver.resolveTxt()` + * * `resolver.reverse()` + * * `resolver.setServers()` + * @since v8.3.0 + */ + export class Resolver { + constructor(options?: ResolverOptions); + /** + * Cancel all outstanding DNS queries made by this resolver. The corresponding + * callbacks will be called with an error with code `ECANCELLED`. + * @since v8.3.0 + */ + cancel(): void; + getServers: typeof getServers; + resolve: typeof resolve; + resolve4: typeof resolve4; + resolve6: typeof resolve6; + resolveAny: typeof resolveAny; + resolveCaa: typeof resolveCaa; + resolveCname: typeof resolveCname; + resolveMx: typeof resolveMx; + resolveNaptr: typeof resolveNaptr; + resolveNs: typeof resolveNs; + resolvePtr: typeof resolvePtr; + resolveSoa: typeof resolveSoa; + resolveSrv: typeof resolveSrv; + resolveTlsa: typeof resolveTlsa; + resolveTxt: typeof resolveTxt; + reverse: typeof reverse; + /** + * The resolver instance will send its requests from the specified IP address. + * This allows programs to specify outbound interfaces when used on multi-homed + * systems. + * + * If a v4 or v6 address is not specified, it is set to the default and the + * operating system will choose a local address automatically. + * + * The resolver will use the v4 local address when making requests to IPv4 DNS + * servers, and the v6 local address when making requests to IPv6 DNS servers. + * The `rrtype` of resolution requests has no impact on the local address used. + * @since v15.1.0, v14.17.0 + * @param [ipv4='0.0.0.0'] A string representation of an IPv4 address. + * @param [ipv6='::0'] A string representation of an IPv6 address. + */ + setLocalAddress(ipv4?: string, ipv6?: string): void; + setServers: typeof setServers; + } + export { dnsPromises as promises }; +} +declare module "node:dns" { + export * from "dns"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dns/promises.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dns/promises.d.ts new file mode 100644 index 00000000..a7ba9bb2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/dns/promises.d.ts @@ -0,0 +1,503 @@ +/** + * The `dns.promises` API provides an alternative set of asynchronous DNS methods + * that return `Promise` objects rather than using callbacks. The API is accessible + * via `import { promises as dnsPromises } from 'node:dns'` or `import dnsPromises from 'node:dns/promises'`. + * @since v10.6.0 + */ +declare module "dns/promises" { + import { + AnyRecord, + CaaRecord, + LookupAddress, + LookupAllOptions, + LookupOneOptions, + LookupOptions, + MxRecord, + NaptrRecord, + RecordWithTtl, + ResolveOptions, + ResolverOptions, + ResolveWithTtlOptions, + SoaRecord, + SrvRecord, + TlsaRecord, + } from "node:dns"; + /** + * Returns an array of IP address strings, formatted according to [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6), + * that are currently configured for DNS resolution. A string will include a port + * section if a custom port is used. + * + * ```js + * [ + * '4.4.4.4', + * '2001:4860:4860::8888', + * '4.4.4.4:1053', + * '[2001:4860:4860::8888]:1053', + * ] + * ``` + * @since v10.6.0 + */ + function getServers(): string[]; + /** + * Resolves a host name (e.g. `'nodejs.org'`) into the first found A (IPv4) or + * AAAA (IPv6) record. All `option` properties are optional. If `options` is an + * integer, then it must be `4` or `6` – if `options` is not provided, then IPv4 + * and IPv6 addresses are both returned if found. + * + * With the `all` option set to `true`, the `Promise` is resolved with `addresses` being an array of objects with the properties `address` and `family`. + * + * On error, the `Promise` is rejected with an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object, where `err.code` is the error code. + * Keep in mind that `err.code` will be set to `'ENOTFOUND'` not only when + * the host name does not exist but also when the lookup fails in other ways + * such as no available file descriptors. + * + * [`dnsPromises.lookup()`](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnspromiseslookuphostname-options) does not necessarily have anything to do with the DNS + * protocol. The implementation uses an operating system facility that can + * associate names with addresses and vice versa. This implementation can have + * subtle but important consequences on the behavior of any Node.js program. Please + * take some time to consult the [Implementation considerations section](https://nodejs.org/docs/latest-v20.x/api/dns.html#implementation-considerations) before + * using `dnsPromises.lookup()`. + * + * Example usage: + * + * ```js + * import dns from 'node:dns'; + * const dnsPromises = dns.promises; + * const options = { + * family: 6, + * hints: dns.ADDRCONFIG | dns.V4MAPPED, + * }; + * + * dnsPromises.lookup('example.com', options).then((result) => { + * console.log('address: %j family: IPv%s', result.address, result.family); + * // address: "2606:2800:220:1:248:1893:25c8:1946" family: IPv6 + * }); + * + * // When options.all is true, the result will be an Array. + * options.all = true; + * dnsPromises.lookup('example.com', options).then((result) => { + * console.log('addresses: %j', result); + * // addresses: [{"address":"2606:2800:220:1:248:1893:25c8:1946","family":6}] + * }); + * ``` + * @since v10.6.0 + */ + function lookup(hostname: string, family: number): Promise; + function lookup(hostname: string, options: LookupOneOptions): Promise; + function lookup(hostname: string, options: LookupAllOptions): Promise; + function lookup(hostname: string, options: LookupOptions): Promise; + function lookup(hostname: string): Promise; + /** + * Resolves the given `address` and `port` into a host name and service using + * the operating system's underlying `getnameinfo` implementation. + * + * If `address` is not a valid IP address, a `TypeError` will be thrown. + * The `port` will be coerced to a number. If it is not a legal port, a `TypeError` will be thrown. + * + * On error, the `Promise` is rejected with an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object, where `err.code` is the error code. + * + * ```js + * import dnsPromises from 'node:dns'; + * dnsPromises.lookupService('127.0.0.1', 22).then((result) => { + * console.log(result.hostname, result.service); + * // Prints: localhost ssh + * }); + * ``` + * @since v10.6.0 + */ + function lookupService( + address: string, + port: number, + ): Promise<{ + hostname: string; + service: string; + }>; + /** + * Uses the DNS protocol to resolve a host name (e.g. `'nodejs.org'`) into an array + * of the resource records. When successful, the `Promise` is resolved with an + * array of resource records. The type and structure of individual results vary + * based on `rrtype`: + * + * + * + * On error, the `Promise` is rejected with an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object, where `err.code` + * is one of the [DNS error codes](https://nodejs.org/docs/latest-v20.x/api/dns.html#error-codes). + * @since v10.6.0 + * @param hostname Host name to resolve. + * @param [rrtype='A'] Resource record type. + */ + function resolve(hostname: string): Promise; + function resolve(hostname: string, rrtype: "A" | "AAAA" | "CNAME" | "NS" | "PTR"): Promise; + function resolve(hostname: string, rrtype: "ANY"): Promise; + function resolve(hostname: string, rrtype: "CAA"): Promise; + function resolve(hostname: string, rrtype: "MX"): Promise; + function resolve(hostname: string, rrtype: "NAPTR"): Promise; + function resolve(hostname: string, rrtype: "SOA"): Promise; + function resolve(hostname: string, rrtype: "SRV"): Promise; + function resolve(hostname: string, rrtype: "TLSA"): Promise; + function resolve(hostname: string, rrtype: "TXT"): Promise; + function resolve(hostname: string, rrtype: string): Promise< + | string[] + | CaaRecord[] + | MxRecord[] + | NaptrRecord[] + | SoaRecord + | SrvRecord[] + | TlsaRecord[] + | string[][] + | AnyRecord[] + >; + /** + * Uses the DNS protocol to resolve IPv4 addresses (`A` records) for the `hostname`. On success, the `Promise` is resolved with an array of IPv4 + * addresses (e.g. `['74.125.79.104', '74.125.79.105', '74.125.79.106']`). + * @since v10.6.0 + * @param hostname Host name to resolve. + */ + function resolve4(hostname: string): Promise; + function resolve4(hostname: string, options: ResolveWithTtlOptions): Promise; + function resolve4(hostname: string, options: ResolveOptions): Promise; + /** + * Uses the DNS protocol to resolve IPv6 addresses (`AAAA` records) for the `hostname`. On success, the `Promise` is resolved with an array of IPv6 + * addresses. + * @since v10.6.0 + * @param hostname Host name to resolve. + */ + function resolve6(hostname: string): Promise; + function resolve6(hostname: string, options: ResolveWithTtlOptions): Promise; + function resolve6(hostname: string, options: ResolveOptions): Promise; + /** + * Uses the DNS protocol to resolve all records (also known as `ANY` or `*` query). + * On success, the `Promise` is resolved with an array containing various types of + * records. Each object has a property `type` that indicates the type of the + * current record. And depending on the `type`, additional properties will be + * present on the object: + * + * + * + * Here is an example of the result object: + * + * ```js + * [ { type: 'A', address: '127.0.0.1', ttl: 299 }, + * { type: 'CNAME', value: 'example.com' }, + * { type: 'MX', exchange: 'alt4.aspmx.l.example.com', priority: 50 }, + * { type: 'NS', value: 'ns1.example.com' }, + * { type: 'TXT', entries: [ 'v=spf1 include:_spf.example.com ~all' ] }, + * { type: 'SOA', + * nsname: 'ns1.example.com', + * hostmaster: 'admin.example.com', + * serial: 156696742, + * refresh: 900, + * retry: 900, + * expire: 1800, + * minttl: 60 } ] + * ``` + * @since v10.6.0 + */ + function resolveAny(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve `CAA` records for the `hostname`. On success, + * the `Promise` is resolved with an array of objects containing available + * certification authority authorization records available for the `hostname` (e.g. `[{critical: 0, iodef: 'mailto:pki@example.com'},{critical: 128, issue: 'pki.example.com'}]`). + * @since v15.0.0, v14.17.0 + */ + function resolveCaa(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve `CNAME` records for the `hostname`. On success, + * the `Promise` is resolved with an array of canonical name records available for + * the `hostname` (e.g. `['bar.example.com']`). + * @since v10.6.0 + */ + function resolveCname(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve mail exchange records (`MX` records) for the `hostname`. On success, the `Promise` is resolved with an array of objects + * containing both a `priority` and `exchange` property (e.g.`[{priority: 10, exchange: 'mx.example.com'}, ...]`). + * @since v10.6.0 + */ + function resolveMx(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve regular expression-based records (`NAPTR` records) for the `hostname`. On success, the `Promise` is resolved with an array + * of objects with the following properties: + * + * * `flags` + * * `service` + * * `regexp` + * * `replacement` + * * `order` + * * `preference` + * + * ```js + * { + * flags: 's', + * service: 'SIP+D2U', + * regexp: '', + * replacement: '_sip._udp.example.com', + * order: 30, + * preference: 100 + * } + * ``` + * @since v10.6.0 + */ + function resolveNaptr(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve name server records (`NS` records) for the `hostname`. On success, the `Promise` is resolved with an array of name server + * records available for `hostname` (e.g.`['ns1.example.com', 'ns2.example.com']`). + * @since v10.6.0 + */ + function resolveNs(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve pointer records (`PTR` records) for the `hostname`. On success, the `Promise` is resolved with an array of strings + * containing the reply records. + * @since v10.6.0 + */ + function resolvePtr(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve a start of authority record (`SOA` record) for + * the `hostname`. On success, the `Promise` is resolved with an object with the + * following properties: + * + * * `nsname` + * * `hostmaster` + * * `serial` + * * `refresh` + * * `retry` + * * `expire` + * * `minttl` + * + * ```js + * { + * nsname: 'ns.example.com', + * hostmaster: 'root.example.com', + * serial: 2013101809, + * refresh: 10000, + * retry: 2400, + * expire: 604800, + * minttl: 3600 + * } + * ``` + * @since v10.6.0 + */ + function resolveSoa(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve service records (`SRV` records) for the `hostname`. On success, the `Promise` is resolved with an array of objects with + * the following properties: + * + * * `priority` + * * `weight` + * * `port` + * * `name` + * + * ```js + * { + * priority: 10, + * weight: 5, + * port: 21223, + * name: 'service.example.com' + * } + * ``` + * @since v10.6.0 + */ + function resolveSrv(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve certificate associations (`TLSA` records) for + * the `hostname`. On success, the `Promise` is resolved with an array of objectsAdd commentMore actions + * with these properties: + * + * * `certUsage` + * * `selector` + * * `match` + * * `data` + * + * ```js + * { + * certUsage: 3, + * selector: 1, + * match: 1, + * data: [ArrayBuffer] + * } + * ``` + * @since v22.15.0 + */ + function resolveTlsa(hostname: string): Promise; + /** + * Uses the DNS protocol to resolve text queries (`TXT` records) for the `hostname`. On success, the `Promise` is resolved with a two-dimensional array + * of the text records available for `hostname` (e.g.`[ ['v=spf1 ip4:0.0.0.0 ', '~all' ] ]`). Each sub-array contains TXT chunks of + * one record. Depending on the use case, these could be either joined together or + * treated separately. + * @since v10.6.0 + */ + function resolveTxt(hostname: string): Promise; + /** + * Performs a reverse DNS query that resolves an IPv4 or IPv6 address to an + * array of host names. + * + * On error, the `Promise` is rejected with an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object, where `err.code` + * is one of the [DNS error codes](https://nodejs.org/docs/latest-v20.x/api/dns.html#error-codes). + * @since v10.6.0 + */ + function reverse(ip: string): Promise; + /** + * Get the default value for `verbatim` in {@link lookup} and [dnsPromises.lookup()](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnspromiseslookuphostname-options). + * The value could be: + * + * * `ipv4first`: for `verbatim` defaulting to `false`. + * * `verbatim`: for `verbatim` defaulting to `true`. + * @since v20.1.0 + */ + function getDefaultResultOrder(): "ipv4first" | "verbatim"; + /** + * Sets the IP address and port of servers to be used when performing DNS + * resolution. The `servers` argument is an array of [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6) formatted + * addresses. If the port is the IANA default DNS port (53) it can be omitted. + * + * ```js + * dnsPromises.setServers([ + * '4.4.4.4', + * '[2001:4860:4860::8888]', + * '4.4.4.4:1053', + * '[2001:4860:4860::8888]:1053', + * ]); + * ``` + * + * An error will be thrown if an invalid address is provided. + * + * The `dnsPromises.setServers()` method must not be called while a DNS query is in + * progress. + * + * This method works much like [resolve.conf](https://man7.org/linux/man-pages/man5/resolv.conf.5.html). + * That is, if attempting to resolve with the first server provided results in a `NOTFOUND` error, the `resolve()` method will _not_ attempt to resolve with + * subsequent servers provided. Fallback DNS servers will only be used if the + * earlier ones time out or result in some other error. + * @since v10.6.0 + * @param servers array of `RFC 5952` formatted addresses + */ + function setServers(servers: readonly string[]): void; + /** + * Set the default value of `order` in `dns.lookup()` and `{@link lookup}`. The value could be: + * + * * `ipv4first`: sets default `order` to `ipv4first`. + * * `ipv6first`: sets default `order` to `ipv6first`. + * * `verbatim`: sets default `order` to `verbatim`. + * + * The default is `verbatim` and [dnsPromises.setDefaultResultOrder()](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnspromisessetdefaultresultorderorder) + * have higher priority than [`--dns-result-order`](https://nodejs.org/docs/latest-v20.x/api/cli.html#--dns-result-orderorder). + * When using [worker threads](https://nodejs.org/docs/latest-v20.x/api/worker_threads.html), [`dnsPromises.setDefaultResultOrder()`](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnspromisessetdefaultresultorderorder) + * from the main thread won't affect the default dns orders in workers. + * @since v16.4.0, v14.18.0 + * @param order must be `'ipv4first'`, `'ipv6first'` or `'verbatim'`. + */ + function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void; + // Error codes + const NODATA: "ENODATA"; + const FORMERR: "EFORMERR"; + const SERVFAIL: "ESERVFAIL"; + const NOTFOUND: "ENOTFOUND"; + const NOTIMP: "ENOTIMP"; + const REFUSED: "EREFUSED"; + const BADQUERY: "EBADQUERY"; + const BADNAME: "EBADNAME"; + const BADFAMILY: "EBADFAMILY"; + const BADRESP: "EBADRESP"; + const CONNREFUSED: "ECONNREFUSED"; + const TIMEOUT: "ETIMEOUT"; + const EOF: "EOF"; + const FILE: "EFILE"; + const NOMEM: "ENOMEM"; + const DESTRUCTION: "EDESTRUCTION"; + const BADSTR: "EBADSTR"; + const BADFLAGS: "EBADFLAGS"; + const NONAME: "ENONAME"; + const BADHINTS: "EBADHINTS"; + const NOTINITIALIZED: "ENOTINITIALIZED"; + const LOADIPHLPAPI: "ELOADIPHLPAPI"; + const ADDRGETNETWORKPARAMS: "EADDRGETNETWORKPARAMS"; + const CANCELLED: "ECANCELLED"; + + /** + * An independent resolver for DNS requests. + * + * Creating a new resolver uses the default server settings. Setting + * the servers used for a resolver using [`resolver.setServers()`](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnspromisessetserversservers) does not affect + * other resolvers: + * + * ```js + * import { promises } from 'node:dns'; + * const resolver = new promises.Resolver(); + * resolver.setServers(['4.4.4.4']); + * + * // This request will use the server at 4.4.4.4, independent of global settings. + * resolver.resolve4('example.org').then((addresses) => { + * // ... + * }); + * + * // Alternatively, the same code can be written using async-await style. + * (async function() { + * const addresses = await resolver.resolve4('example.org'); + * })(); + * ``` + * + * The following methods from the `dnsPromises` API are available: + * + * * `resolver.getServers()` + * * `resolver.resolve()` + * * `resolver.resolve4()` + * * `resolver.resolve6()` + * * `resolver.resolveAny()` + * * `resolver.resolveCaa()` + * * `resolver.resolveCname()` + * * `resolver.resolveMx()` + * * `resolver.resolveNaptr()` + * * `resolver.resolveNs()` + * * `resolver.resolvePtr()` + * * `resolver.resolveSoa()` + * * `resolver.resolveSrv()` + * * `resolver.resolveTxt()` + * * `resolver.reverse()` + * * `resolver.setServers()` + * @since v10.6.0 + */ + class Resolver { + constructor(options?: ResolverOptions); + /** + * Cancel all outstanding DNS queries made by this resolver. The corresponding + * callbacks will be called with an error with code `ECANCELLED`. + * @since v8.3.0 + */ + cancel(): void; + getServers: typeof getServers; + resolve: typeof resolve; + resolve4: typeof resolve4; + resolve6: typeof resolve6; + resolveAny: typeof resolveAny; + resolveCaa: typeof resolveCaa; + resolveCname: typeof resolveCname; + resolveMx: typeof resolveMx; + resolveNaptr: typeof resolveNaptr; + resolveNs: typeof resolveNs; + resolvePtr: typeof resolvePtr; + resolveSoa: typeof resolveSoa; + resolveSrv: typeof resolveSrv; + resolveTlsa: typeof resolveTlsa; + resolveTxt: typeof resolveTxt; + reverse: typeof reverse; + /** + * The resolver instance will send its requests from the specified IP address. + * This allows programs to specify outbound interfaces when used on multi-homed + * systems. + * + * If a v4 or v6 address is not specified, it is set to the default and the + * operating system will choose a local address automatically. + * + * The resolver will use the v4 local address when making requests to IPv4 DNS + * servers, and the v6 local address when making requests to IPv6 DNS servers. + * The `rrtype` of resolution requests has no impact on the local address used. + * @since v15.1.0, v14.17.0 + * @param [ipv4='0.0.0.0'] A string representation of an IPv4 address. + * @param [ipv6='::0'] A string representation of an IPv6 address. + */ + setLocalAddress(ipv4?: string, ipv6?: string): void; + setServers: typeof setServers; + } +} +declare module "node:dns/promises" { + export * from "dns/promises"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/domain.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/domain.d.ts new file mode 100644 index 00000000..ba8a02c1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/domain.d.ts @@ -0,0 +1,170 @@ +/** + * **This module is pending deprecation.** Once a replacement API has been + * finalized, this module will be fully deprecated. Most developers should + * **not** have cause to use this module. Users who absolutely must have + * the functionality that domains provide may rely on it for the time being + * but should expect to have to migrate to a different solution + * in the future. + * + * Domains provide a way to handle multiple different IO operations as a + * single group. If any of the event emitters or callbacks registered to a + * domain emit an `'error'` event, or throw an error, then the domain object + * will be notified, rather than losing the context of the error in the `process.on('uncaughtException')` handler, or causing the program to + * exit immediately with an error code. + * @deprecated Since v1.4.2 - Deprecated + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/domain.js) + */ +declare module "domain" { + import EventEmitter = require("node:events"); + /** + * The `Domain` class encapsulates the functionality of routing errors and + * uncaught exceptions to the active `Domain` object. + * + * To handle the errors that it catches, listen to its `'error'` event. + */ + class Domain extends EventEmitter { + /** + * An array of timers and event emitters that have been explicitly added + * to the domain. + */ + members: Array; + /** + * The `enter()` method is plumbing used by the `run()`, `bind()`, and `intercept()` methods to set the active domain. It sets `domain.active` and `process.domain` to the domain, and implicitly + * pushes the domain onto the domain + * stack managed by the domain module (see {@link exit} for details on the + * domain stack). The call to `enter()` delimits the beginning of a chain of + * asynchronous calls and I/O operations bound to a domain. + * + * Calling `enter()` changes only the active domain, and does not alter the domain + * itself. `enter()` and `exit()` can be called an arbitrary number of times on a + * single domain. + */ + enter(): void; + /** + * The `exit()` method exits the current domain, popping it off the domain stack. + * Any time execution is going to switch to the context of a different chain of + * asynchronous calls, it's important to ensure that the current domain is exited. + * The call to `exit()` delimits either the end of or an interruption to the chain + * of asynchronous calls and I/O operations bound to a domain. + * + * If there are multiple, nested domains bound to the current execution context, `exit()` will exit any domains nested within this domain. + * + * Calling `exit()` changes only the active domain, and does not alter the domain + * itself. `enter()` and `exit()` can be called an arbitrary number of times on a + * single domain. + */ + exit(): void; + /** + * Run the supplied function in the context of the domain, implicitly + * binding all event emitters, timers, and low-level requests that are + * created in that context. Optionally, arguments can be passed to + * the function. + * + * This is the most basic way to use a domain. + * + * ```js + * import domain from 'node:domain'; + * import fs from 'node:fs'; + * const d = domain.create(); + * d.on('error', (er) => { + * console.error('Caught error!', er); + * }); + * d.run(() => { + * process.nextTick(() => { + * setTimeout(() => { // Simulating some various async stuff + * fs.open('non-existent file', 'r', (er, fd) => { + * if (er) throw er; + * // proceed... + * }); + * }, 100); + * }); + * }); + * ``` + * + * In this example, the `d.on('error')` handler will be triggered, rather + * than crashing the program. + */ + run(fn: (...args: any[]) => T, ...args: any[]): T; + /** + * Explicitly adds an emitter to the domain. If any event handlers called by + * the emitter throw an error, or if the emitter emits an `'error'` event, it + * will be routed to the domain's `'error'` event, just like with implicit + * binding. + * + * This also works with timers that are returned from `setInterval()` and `setTimeout()`. If their callback function throws, it will be caught by + * the domain `'error'` handler. + * + * If the Timer or `EventEmitter` was already bound to a domain, it is removed + * from that one, and bound to this one instead. + * @param emitter emitter or timer to be added to the domain + */ + add(emitter: EventEmitter | NodeJS.Timer): void; + /** + * The opposite of {@link add}. Removes domain handling from the + * specified emitter. + * @param emitter emitter or timer to be removed from the domain + */ + remove(emitter: EventEmitter | NodeJS.Timer): void; + /** + * The returned function will be a wrapper around the supplied callback + * function. When the returned function is called, any errors that are + * thrown will be routed to the domain's `'error'` event. + * + * ```js + * const d = domain.create(); + * + * function readSomeFile(filename, cb) { + * fs.readFile(filename, 'utf8', d.bind((er, data) => { + * // If this throws, it will also be passed to the domain. + * return cb(er, data ? JSON.parse(data) : null); + * })); + * } + * + * d.on('error', (er) => { + * // An error occurred somewhere. If we throw it now, it will crash the program + * // with the normal line number and stack message. + * }); + * ``` + * @param callback The callback function + * @return The bound function + */ + bind(callback: T): T; + /** + * This method is almost identical to {@link bind}. However, in + * addition to catching thrown errors, it will also intercept `Error` objects sent as the first argument to the function. + * + * In this way, the common `if (err) return callback(err);` pattern can be replaced + * with a single error handler in a single place. + * + * ```js + * const d = domain.create(); + * + * function readSomeFile(filename, cb) { + * fs.readFile(filename, 'utf8', d.intercept((data) => { + * // Note, the first argument is never passed to the + * // callback since it is assumed to be the 'Error' argument + * // and thus intercepted by the domain. + * + * // If this throws, it will also be passed to the domain + * // so the error-handling logic can be moved to the 'error' + * // event on the domain instead of being repeated throughout + * // the program. + * return cb(null, JSON.parse(data)); + * })); + * } + * + * d.on('error', (er) => { + * // An error occurred somewhere. If we throw it now, it will crash the program + * // with the normal line number and stack message. + * }); + * ``` + * @param callback The callback function + * @return The intercepted function + */ + intercept(callback: T): T; + } + function create(): Domain; +} +declare module "node:domain" { + export * from "domain"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/events.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/events.d.ts new file mode 100644 index 00000000..c336a289 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/events.d.ts @@ -0,0 +1,976 @@ +/** + * Much of the Node.js core API is built around an idiomatic asynchronous + * event-driven architecture in which certain kinds of objects (called "emitters") + * emit named events that cause `Function` objects ("listeners") to be called. + * + * For instance: a `net.Server` object emits an event each time a peer + * connects to it; a `fs.ReadStream` emits an event when the file is opened; + * a `stream` emits an event whenever data is available to be read. + * + * All objects that emit events are instances of the `EventEmitter` class. These + * objects expose an `eventEmitter.on()` function that allows one or more + * functions to be attached to named events emitted by the object. Typically, + * event names are camel-cased strings but any valid JavaScript property key + * can be used. + * + * When the `EventEmitter` object emits an event, all of the functions attached + * to that specific event are called _synchronously_. Any values returned by the + * called listeners are _ignored_ and discarded. + * + * The following example shows a simple `EventEmitter` instance with a single + * listener. The `eventEmitter.on()` method is used to register listeners, while + * the `eventEmitter.emit()` method is used to trigger the event. + * + * ```js + * import { EventEmitter } from 'node:events'; + * + * class MyEmitter extends EventEmitter {} + * + * const myEmitter = new MyEmitter(); + * myEmitter.on('event', () => { + * console.log('an event occurred!'); + * }); + * myEmitter.emit('event'); + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/events.js) + */ +declare module "events" { + import { AsyncResource, AsyncResourceOptions } from "node:async_hooks"; + interface EventEmitterOptions { + /** + * Enables automatic capturing of promise rejection. + */ + captureRejections?: boolean | undefined; + } + interface StaticEventEmitterOptions { + /** + * Can be used to cancel awaiting events. + */ + signal?: AbortSignal | undefined; + } + interface StaticEventEmitterIteratorOptions extends StaticEventEmitterOptions { + /** + * Names of events that will end the iteration. + */ + close?: string[] | undefined; + /** + * The high watermark. The emitter is paused every time the size of events being buffered is higher than it. + * Supported only on emitters implementing `pause()` and `resume()` methods. + * @default Number.MAX_SAFE_INTEGER + */ + highWaterMark?: number | undefined; + /** + * The low watermark. The emitter is resumed every time the size of events being buffered is lower than it. + * Supported only on emitters implementing `pause()` and `resume()` methods. + * @default 1 + */ + lowWaterMark?: number | undefined; + } + interface EventEmitter = DefaultEventMap> extends NodeJS.EventEmitter {} + type EventMap = Record | DefaultEventMap; + type DefaultEventMap = [never]; + type AnyRest = [...args: any[]]; + type Args = T extends DefaultEventMap ? AnyRest : ( + K extends keyof T ? T[K] : never + ); + type Key = T extends DefaultEventMap ? string | symbol : K | keyof T; + type Key2 = T extends DefaultEventMap ? string | symbol : K & keyof T; + type Listener = T extends DefaultEventMap ? F : ( + K extends keyof T ? ( + T[K] extends unknown[] ? (...args: T[K]) => void : never + ) + : never + ); + type Listener1 = Listener void>; + type Listener2 = Listener; + + /** + * The `EventEmitter` class is defined and exposed by the `node:events` module: + * + * ```js + * import { EventEmitter } from 'node:events'; + * ``` + * + * All `EventEmitter`s emit the event `'newListener'` when new listeners are + * added and `'removeListener'` when existing listeners are removed. + * + * It supports the following option: + * @since v0.1.26 + */ + class EventEmitter = DefaultEventMap> { + constructor(options?: EventEmitterOptions); + + [EventEmitter.captureRejectionSymbol]?(error: Error, event: Key, ...args: Args): void; + + /** + * Creates a `Promise` that is fulfilled when the `EventEmitter` emits the given + * event or that is rejected if the `EventEmitter` emits `'error'` while waiting. + * The `Promise` will resolve with an array of all the arguments emitted to the + * given event. + * + * This method is intentionally generic and works with the web platform [EventTarget](https://dom.spec.whatwg.org/#interface-eventtarget) interface, which has no special`'error'` event + * semantics and does not listen to the `'error'` event. + * + * ```js + * import { once, EventEmitter } from 'node:events'; + * import process from 'node:process'; + * + * const ee = new EventEmitter(); + * + * process.nextTick(() => { + * ee.emit('myevent', 42); + * }); + * + * const [value] = await once(ee, 'myevent'); + * console.log(value); + * + * const err = new Error('kaboom'); + * process.nextTick(() => { + * ee.emit('error', err); + * }); + * + * try { + * await once(ee, 'myevent'); + * } catch (err) { + * console.error('error happened', err); + * } + * ``` + * + * The special handling of the `'error'` event is only used when `events.once()` is used to wait for another event. If `events.once()` is used to wait for the + * '`error'` event itself, then it is treated as any other kind of event without + * special handling: + * + * ```js + * import { EventEmitter, once } from 'node:events'; + * + * const ee = new EventEmitter(); + * + * once(ee, 'error') + * .then(([err]) => console.log('ok', err.message)) + * .catch((err) => console.error('error', err.message)); + * + * ee.emit('error', new Error('boom')); + * + * // Prints: ok boom + * ``` + * + * An `AbortSignal` can be used to cancel waiting for the event: + * + * ```js + * import { EventEmitter, once } from 'node:events'; + * + * const ee = new EventEmitter(); + * const ac = new AbortController(); + * + * async function foo(emitter, event, signal) { + * try { + * await once(emitter, event, { signal }); + * console.log('event emitted!'); + * } catch (error) { + * if (error.name === 'AbortError') { + * console.error('Waiting for the event was canceled!'); + * } else { + * console.error('There was an error', error.message); + * } + * } + * } + * + * foo(ee, 'foo', ac.signal); + * ac.abort(); // Abort waiting for the event + * ee.emit('foo'); // Prints: Waiting for the event was canceled! + * ``` + * @since v11.13.0, v10.16.0 + */ + static once( + emitter: NodeJS.EventEmitter, + eventName: string | symbol, + options?: StaticEventEmitterOptions, + ): Promise; + static once(emitter: EventTarget, eventName: string, options?: StaticEventEmitterOptions): Promise; + /** + * ```js + * import { on, EventEmitter } from 'node:events'; + * import process from 'node:process'; + * + * const ee = new EventEmitter(); + * + * // Emit later on + * process.nextTick(() => { + * ee.emit('foo', 'bar'); + * ee.emit('foo', 42); + * }); + * + * for await (const event of on(ee, 'foo')) { + * // The execution of this inner block is synchronous and it + * // processes one event at a time (even with await). Do not use + * // if concurrent execution is required. + * console.log(event); // prints ['bar'] [42] + * } + * // Unreachable here + * ``` + * + * Returns an `AsyncIterator` that iterates `eventName` events. It will throw + * if the `EventEmitter` emits `'error'`. It removes all listeners when + * exiting the loop. The `value` returned by each iteration is an array + * composed of the emitted event arguments. + * + * An `AbortSignal` can be used to cancel waiting on events: + * + * ```js + * import { on, EventEmitter } from 'node:events'; + * import process from 'node:process'; + * + * const ac = new AbortController(); + * + * (async () => { + * const ee = new EventEmitter(); + * + * // Emit later on + * process.nextTick(() => { + * ee.emit('foo', 'bar'); + * ee.emit('foo', 42); + * }); + * + * for await (const event of on(ee, 'foo', { signal: ac.signal })) { + * // The execution of this inner block is synchronous and it + * // processes one event at a time (even with await). Do not use + * // if concurrent execution is required. + * console.log(event); // prints ['bar'] [42] + * } + * // Unreachable here + * })(); + * + * process.nextTick(() => ac.abort()); + * ``` + * + * Use the `close` option to specify an array of event names that will end the iteration: + * + * ```js + * import { on, EventEmitter } from 'node:events'; + * import process from 'node:process'; + * + * const ee = new EventEmitter(); + * + * // Emit later on + * process.nextTick(() => { + * ee.emit('foo', 'bar'); + * ee.emit('foo', 42); + * ee.emit('close'); + * }); + * + * for await (const event of on(ee, 'foo', { close: ['close'] })) { + * console.log(event); // prints ['bar'] [42] + * } + * // the loop will exit after 'close' is emitted + * console.log('done'); // prints 'done' + * ``` + * @since v13.6.0, v12.16.0 + * @return An `AsyncIterator` that iterates `eventName` events emitted by the `emitter` + */ + static on( + emitter: NodeJS.EventEmitter, + eventName: string | symbol, + options?: StaticEventEmitterIteratorOptions, + ): NodeJS.AsyncIterator; + static on( + emitter: EventTarget, + eventName: string, + options?: StaticEventEmitterIteratorOptions, + ): NodeJS.AsyncIterator; + /** + * A class method that returns the number of listeners for the given `eventName` registered on the given `emitter`. + * + * ```js + * import { EventEmitter, listenerCount } from 'node:events'; + * + * const myEmitter = new EventEmitter(); + * myEmitter.on('event', () => {}); + * myEmitter.on('event', () => {}); + * console.log(listenerCount(myEmitter, 'event')); + * // Prints: 2 + * ``` + * @since v0.9.12 + * @deprecated Since v3.2.0 - Use `listenerCount` instead. + * @param emitter The emitter to query + * @param eventName The event name + */ + static listenerCount(emitter: NodeJS.EventEmitter, eventName: string | symbol): number; + /** + * Returns a copy of the array of listeners for the event named `eventName`. + * + * For `EventEmitter`s this behaves exactly the same as calling `.listeners` on + * the emitter. + * + * For `EventTarget`s this is the only way to get the event listeners for the + * event target. This is useful for debugging and diagnostic purposes. + * + * ```js + * import { getEventListeners, EventEmitter } from 'node:events'; + * + * { + * const ee = new EventEmitter(); + * const listener = () => console.log('Events are fun'); + * ee.on('foo', listener); + * console.log(getEventListeners(ee, 'foo')); // [ [Function: listener] ] + * } + * { + * const et = new EventTarget(); + * const listener = () => console.log('Events are fun'); + * et.addEventListener('foo', listener); + * console.log(getEventListeners(et, 'foo')); // [ [Function: listener] ] + * } + * ``` + * @since v15.2.0, v14.17.0 + */ + static getEventListeners(emitter: EventTarget | NodeJS.EventEmitter, name: string | symbol): Function[]; + /** + * Returns the currently set max amount of listeners. + * + * For `EventEmitter`s this behaves exactly the same as calling `.getMaxListeners` on + * the emitter. + * + * For `EventTarget`s this is the only way to get the max event listeners for the + * event target. If the number of event handlers on a single EventTarget exceeds + * the max set, the EventTarget will print a warning. + * + * ```js + * import { getMaxListeners, setMaxListeners, EventEmitter } from 'node:events'; + * + * { + * const ee = new EventEmitter(); + * console.log(getMaxListeners(ee)); // 10 + * setMaxListeners(11, ee); + * console.log(getMaxListeners(ee)); // 11 + * } + * { + * const et = new EventTarget(); + * console.log(getMaxListeners(et)); // 10 + * setMaxListeners(11, et); + * console.log(getMaxListeners(et)); // 11 + * } + * ``` + * @since v19.9.0 + */ + static getMaxListeners(emitter: EventTarget | NodeJS.EventEmitter): number; + /** + * ```js + * import { setMaxListeners, EventEmitter } from 'node:events'; + * + * const target = new EventTarget(); + * const emitter = new EventEmitter(); + * + * setMaxListeners(5, target, emitter); + * ``` + * @since v15.4.0 + * @param n A non-negative number. The maximum number of listeners per `EventTarget` event. + * @param eventTargets Zero or more {EventTarget} or {EventEmitter} instances. If none are specified, `n` is set as the default max for all newly created {EventTarget} and {EventEmitter} + * objects. + */ + static setMaxListeners(n?: number, ...eventTargets: Array): void; + /** + * Listens once to the `abort` event on the provided `signal`. + * + * Listening to the `abort` event on abort signals is unsafe and may + * lead to resource leaks since another third party with the signal can + * call `e.stopImmediatePropagation()`. Unfortunately Node.js cannot change + * this since it would violate the web standard. Additionally, the original + * API makes it easy to forget to remove listeners. + * + * This API allows safely using `AbortSignal`s in Node.js APIs by solving these + * two issues by listening to the event such that `stopImmediatePropagation` does + * not prevent the listener from running. + * + * Returns a disposable so that it may be unsubscribed from more easily. + * + * ```js + * import { addAbortListener } from 'node:events'; + * + * function example(signal) { + * let disposable; + * try { + * signal.addEventListener('abort', (e) => e.stopImmediatePropagation()); + * disposable = addAbortListener(signal, (e) => { + * // Do something when signal is aborted. + * }); + * } finally { + * disposable?.[Symbol.dispose](); + * } + * } + * ``` + * @since v20.5.0 + * @return Disposable that removes the `abort` listener. + */ + static addAbortListener(signal: AbortSignal, resource: (event: Event) => void): Disposable; + /** + * This symbol shall be used to install a listener for only monitoring `'error'` events. Listeners installed using this symbol are called before the regular `'error'` listeners are called. + * + * Installing a listener using this symbol does not change the behavior once an `'error'` event is emitted. Therefore, the process will still crash if no + * regular `'error'` listener is installed. + * @since v13.6.0, v12.17.0 + */ + static readonly errorMonitor: unique symbol; + /** + * Value: `Symbol.for('nodejs.rejection')` + * + * See how to write a custom `rejection handler`. + * @since v13.4.0, v12.16.0 + */ + static readonly captureRejectionSymbol: unique symbol; + /** + * Value: [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) + * + * Change the default `captureRejections` option on all new `EventEmitter` objects. + * @since v13.4.0, v12.16.0 + */ + static captureRejections: boolean; + /** + * By default, a maximum of `10` listeners can be registered for any single + * event. This limit can be changed for individual `EventEmitter` instances + * using the `emitter.setMaxListeners(n)` method. To change the default + * for _all_`EventEmitter` instances, the `events.defaultMaxListeners` property + * can be used. If this value is not a positive number, a `RangeError` is thrown. + * + * Take caution when setting the `events.defaultMaxListeners` because the + * change affects _all_ `EventEmitter` instances, including those created before + * the change is made. However, calling `emitter.setMaxListeners(n)` still has + * precedence over `events.defaultMaxListeners`. + * + * This is not a hard limit. The `EventEmitter` instance will allow + * more listeners to be added but will output a trace warning to stderr indicating + * that a "possible EventEmitter memory leak" has been detected. For any single + * `EventEmitter`, the `emitter.getMaxListeners()` and `emitter.setMaxListeners()` methods can be used to + * temporarily avoid this warning: + * + * ```js + * import { EventEmitter } from 'node:events'; + * const emitter = new EventEmitter(); + * emitter.setMaxListeners(emitter.getMaxListeners() + 1); + * emitter.once('event', () => { + * // do stuff + * emitter.setMaxListeners(Math.max(emitter.getMaxListeners() - 1, 0)); + * }); + * ``` + * + * The `--trace-warnings` command-line flag can be used to display the + * stack trace for such warnings. + * + * The emitted warning can be inspected with `process.on('warning')` and will + * have the additional `emitter`, `type`, and `count` properties, referring to + * the event emitter instance, the event's name and the number of attached + * listeners, respectively. + * Its `name` property is set to `'MaxListenersExceededWarning'`. + * @since v0.11.2 + */ + static defaultMaxListeners: number; + } + import internal = require("node:events"); + namespace EventEmitter { + // Should just be `export { EventEmitter }`, but that doesn't work in TypeScript 3.4 + export { internal as EventEmitter }; + export interface Abortable { + /** + * When provided the corresponding `AbortController` can be used to cancel an asynchronous action. + */ + signal?: AbortSignal | undefined; + } + + export interface EventEmitterReferencingAsyncResource extends AsyncResource { + readonly eventEmitter: EventEmitterAsyncResource; + } + + export interface EventEmitterAsyncResourceOptions extends AsyncResourceOptions, EventEmitterOptions { + /** + * The type of async event, this is required when instantiating `EventEmitterAsyncResource` + * directly rather than as a child class. + * @default new.target.name if instantiated as a child class. + */ + name?: string | undefined; + } + + /** + * Integrates `EventEmitter` with `AsyncResource` for `EventEmitter`s that + * require manual async tracking. Specifically, all events emitted by instances + * of `events.EventEmitterAsyncResource` will run within its `async context`. + * + * ```js + * import { EventEmitterAsyncResource, EventEmitter } from 'node:events'; + * import { notStrictEqual, strictEqual } from 'node:assert'; + * import { executionAsyncId, triggerAsyncId } from 'node:async_hooks'; + * + * // Async tracking tooling will identify this as 'Q'. + * const ee1 = new EventEmitterAsyncResource({ name: 'Q' }); + * + * // 'foo' listeners will run in the EventEmitters async context. + * ee1.on('foo', () => { + * strictEqual(executionAsyncId(), ee1.asyncId); + * strictEqual(triggerAsyncId(), ee1.triggerAsyncId); + * }); + * + * const ee2 = new EventEmitter(); + * + * // 'foo' listeners on ordinary EventEmitters that do not track async + * // context, however, run in the same async context as the emit(). + * ee2.on('foo', () => { + * notStrictEqual(executionAsyncId(), ee2.asyncId); + * notStrictEqual(triggerAsyncId(), ee2.triggerAsyncId); + * }); + * + * Promise.resolve().then(() => { + * ee1.emit('foo'); + * ee2.emit('foo'); + * }); + * ``` + * + * The `EventEmitterAsyncResource` class has the same methods and takes the + * same options as `EventEmitter` and `AsyncResource` themselves. + * @since v17.4.0, v16.14.0 + */ + export class EventEmitterAsyncResource extends EventEmitter { + /** + * @param options Only optional in child class. + */ + constructor(options?: EventEmitterAsyncResourceOptions); + /** + * Call all `destroy` hooks. This should only ever be called once. An error will + * be thrown if it is called more than once. This **must** be manually called. If + * the resource is left to be collected by the GC then the `destroy` hooks will + * never be called. + */ + emitDestroy(): void; + /** + * The unique `asyncId` assigned to the resource. + */ + readonly asyncId: number; + /** + * The same triggerAsyncId that is passed to the AsyncResource constructor. + */ + readonly triggerAsyncId: number; + /** + * The returned `AsyncResource` object has an additional `eventEmitter` property + * that provides a reference to this `EventEmitterAsyncResource`. + */ + readonly asyncResource: EventEmitterReferencingAsyncResource; + } + /** + * The `NodeEventTarget` is a Node.js-specific extension to `EventTarget` + * that emulates a subset of the `EventEmitter` API. + * @since v14.5.0 + */ + export interface NodeEventTarget extends EventTarget { + /** + * Node.js-specific extension to the `EventTarget` class that emulates the + * equivalent `EventEmitter` API. The only difference between `addListener()` and + * `addEventListener()` is that `addListener()` will return a reference to the + * `EventTarget`. + * @since v14.5.0 + */ + addListener(type: string, listener: (arg: any) => void): this; + /** + * Node.js-specific extension to the `EventTarget` class that dispatches the + * `arg` to the list of handlers for `type`. + * @since v15.2.0 + * @returns `true` if event listeners registered for the `type` exist, + * otherwise `false`. + */ + emit(type: string, arg: any): boolean; + /** + * Node.js-specific extension to the `EventTarget` class that returns an array + * of event `type` names for which event listeners are registered. + * @since 14.5.0 + */ + eventNames(): string[]; + /** + * Node.js-specific extension to the `EventTarget` class that returns the number + * of event listeners registered for the `type`. + * @since v14.5.0 + */ + listenerCount(type: string): number; + /** + * Node.js-specific extension to the `EventTarget` class that sets the number + * of max event listeners as `n`. + * @since v14.5.0 + */ + setMaxListeners(n: number): void; + /** + * Node.js-specific extension to the `EventTarget` class that returns the number + * of max event listeners. + * @since v14.5.0 + */ + getMaxListeners(): number; + /** + * Node.js-specific alias for `eventTarget.removeEventListener()`. + * @since v14.5.0 + */ + off(type: string, listener: (arg: any) => void, options?: EventListenerOptions): this; + /** + * Node.js-specific alias for `eventTarget.addEventListener()`. + * @since v14.5.0 + */ + on(type: string, listener: (arg: any) => void): this; + /** + * Node.js-specific extension to the `EventTarget` class that adds a `once` + * listener for the given event `type`. This is equivalent to calling `on` + * with the `once` option set to `true`. + * @since v14.5.0 + */ + once(type: string, listener: (arg: any) => void): this; + /** + * Node.js-specific extension to the `EventTarget` class. If `type` is specified, + * removes all registered listeners for `type`, otherwise removes all registered + * listeners. + * @since v14.5.0 + */ + removeAllListeners(type?: string): this; + /** + * Node.js-specific extension to the `EventTarget` class that removes the + * `listener` for the given `type`. The only difference between `removeListener()` + * and `removeEventListener()` is that `removeListener()` will return a reference + * to the `EventTarget`. + * @since v14.5.0 + */ + removeListener(type: string, listener: (arg: any) => void, options?: EventListenerOptions): this; + } + } + global { + namespace NodeJS { + interface EventEmitter = DefaultEventMap> { + [EventEmitter.captureRejectionSymbol]?(error: Error, event: Key, ...args: Args): void; + /** + * Alias for `emitter.on(eventName, listener)`. + * @since v0.1.26 + */ + addListener(eventName: Key, listener: Listener1): this; + /** + * Adds the `listener` function to the end of the listeners array for the event + * named `eventName`. No checks are made to see if the `listener` has already + * been added. Multiple calls passing the same combination of `eventName` and + * `listener` will result in the `listener` being added, and called, multiple times. + * + * ```js + * server.on('connection', (stream) => { + * console.log('someone connected!'); + * }); + * ``` + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * By default, event listeners are invoked in the order they are added. The `emitter.prependListener()` method can be used as an alternative to add the + * event listener to the beginning of the listeners array. + * + * ```js + * import { EventEmitter } from 'node:events'; + * const myEE = new EventEmitter(); + * myEE.on('foo', () => console.log('a')); + * myEE.prependListener('foo', () => console.log('b')); + * myEE.emit('foo'); + * // Prints: + * // b + * // a + * ``` + * @since v0.1.101 + * @param eventName The name of the event. + * @param listener The callback function + */ + on(eventName: Key, listener: Listener1): this; + /** + * Adds a **one-time** `listener` function for the event named `eventName`. The + * next time `eventName` is triggered, this listener is removed and then invoked. + * + * ```js + * server.once('connection', (stream) => { + * console.log('Ah, we have our first user!'); + * }); + * ``` + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * By default, event listeners are invoked in the order they are added. The `emitter.prependOnceListener()` method can be used as an alternative to add the + * event listener to the beginning of the listeners array. + * + * ```js + * import { EventEmitter } from 'node:events'; + * const myEE = new EventEmitter(); + * myEE.once('foo', () => console.log('a')); + * myEE.prependOnceListener('foo', () => console.log('b')); + * myEE.emit('foo'); + * // Prints: + * // b + * // a + * ``` + * @since v0.3.0 + * @param eventName The name of the event. + * @param listener The callback function + */ + once(eventName: Key, listener: Listener1): this; + /** + * Removes the specified `listener` from the listener array for the event named `eventName`. + * + * ```js + * const callback = (stream) => { + * console.log('someone connected!'); + * }; + * server.on('connection', callback); + * // ... + * server.removeListener('connection', callback); + * ``` + * + * `removeListener()` will remove, at most, one instance of a listener from the + * listener array. If any single listener has been added multiple times to the + * listener array for the specified `eventName`, then `removeListener()` must be + * called multiple times to remove each instance. + * + * Once an event is emitted, all listeners attached to it at the + * time of emitting are called in order. This implies that any `removeListener()` or `removeAllListeners()` calls _after_ emitting and _before_ the last listener finishes execution + * will not remove them from`emit()` in progress. Subsequent events behave as expected. + * + * ```js + * import { EventEmitter } from 'node:events'; + * class MyEmitter extends EventEmitter {} + * const myEmitter = new MyEmitter(); + * + * const callbackA = () => { + * console.log('A'); + * myEmitter.removeListener('event', callbackB); + * }; + * + * const callbackB = () => { + * console.log('B'); + * }; + * + * myEmitter.on('event', callbackA); + * + * myEmitter.on('event', callbackB); + * + * // callbackA removes listener callbackB but it will still be called. + * // Internal listener array at time of emit [callbackA, callbackB] + * myEmitter.emit('event'); + * // Prints: + * // A + * // B + * + * // callbackB is now removed. + * // Internal listener array [callbackA] + * myEmitter.emit('event'); + * // Prints: + * // A + * ``` + * + * Because listeners are managed using an internal array, calling this will + * change the position indices of any listener registered _after_ the listener + * being removed. This will not impact the order in which listeners are called, + * but it means that any copies of the listener array as returned by + * the `emitter.listeners()` method will need to be recreated. + * + * When a single function has been added as a handler multiple times for a single + * event (as in the example below), `removeListener()` will remove the most + * recently added instance. In the example the `once('ping')` listener is removed: + * + * ```js + * import { EventEmitter } from 'node:events'; + * const ee = new EventEmitter(); + * + * function pong() { + * console.log('pong'); + * } + * + * ee.on('ping', pong); + * ee.once('ping', pong); + * ee.removeListener('ping', pong); + * + * ee.emit('ping'); + * ee.emit('ping'); + * ``` + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * @since v0.1.26 + */ + removeListener(eventName: Key, listener: Listener1): this; + /** + * Alias for `emitter.removeListener()`. + * @since v10.0.0 + */ + off(eventName: Key, listener: Listener1): this; + /** + * Removes all listeners, or those of the specified `eventName`. + * + * It is bad practice to remove listeners added elsewhere in the code, + * particularly when the `EventEmitter` instance was created by some other + * component or module (e.g. sockets or file streams). + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * @since v0.1.26 + */ + removeAllListeners(eventName?: Key): this; + /** + * By default `EventEmitter`s will print a warning if more than `10` listeners are + * added for a particular event. This is a useful default that helps finding + * memory leaks. The `emitter.setMaxListeners()` method allows the limit to be + * modified for this specific `EventEmitter` instance. The value can be set to `Infinity` (or `0`) to indicate an unlimited number of listeners. + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * @since v0.3.5 + */ + setMaxListeners(n: number): this; + /** + * Returns the current max listener value for the `EventEmitter` which is either + * set by `emitter.setMaxListeners(n)` or defaults to {@link EventEmitter.defaultMaxListeners}. + * @since v1.0.0 + */ + getMaxListeners(): number; + /** + * Returns a copy of the array of listeners for the event named `eventName`. + * + * ```js + * server.on('connection', (stream) => { + * console.log('someone connected!'); + * }); + * console.log(util.inspect(server.listeners('connection'))); + * // Prints: [ [Function] ] + * ``` + * @since v0.1.26 + */ + listeners(eventName: Key): Array>; + /** + * Returns a copy of the array of listeners for the event named `eventName`, + * including any wrappers (such as those created by `.once()`). + * + * ```js + * import { EventEmitter } from 'node:events'; + * const emitter = new EventEmitter(); + * emitter.once('log', () => console.log('log once')); + * + * // Returns a new Array with a function `onceWrapper` which has a property + * // `listener` which contains the original listener bound above + * const listeners = emitter.rawListeners('log'); + * const logFnWrapper = listeners[0]; + * + * // Logs "log once" to the console and does not unbind the `once` event + * logFnWrapper.listener(); + * + * // Logs "log once" to the console and removes the listener + * logFnWrapper(); + * + * emitter.on('log', () => console.log('log persistently')); + * // Will return a new Array with a single function bound by `.on()` above + * const newListeners = emitter.rawListeners('log'); + * + * // Logs "log persistently" twice + * newListeners[0](); + * emitter.emit('log'); + * ``` + * @since v9.4.0 + */ + rawListeners(eventName: Key): Array>; + /** + * Synchronously calls each of the listeners registered for the event named `eventName`, in the order they were registered, passing the supplied arguments + * to each. + * + * Returns `true` if the event had listeners, `false` otherwise. + * + * ```js + * import { EventEmitter } from 'node:events'; + * const myEmitter = new EventEmitter(); + * + * // First listener + * myEmitter.on('event', function firstListener() { + * console.log('Helloooo! first listener'); + * }); + * // Second listener + * myEmitter.on('event', function secondListener(arg1, arg2) { + * console.log(`event with parameters ${arg1}, ${arg2} in second listener`); + * }); + * // Third listener + * myEmitter.on('event', function thirdListener(...args) { + * const parameters = args.join(', '); + * console.log(`event with parameters ${parameters} in third listener`); + * }); + * + * console.log(myEmitter.listeners('event')); + * + * myEmitter.emit('event', 1, 2, 3, 4, 5); + * + * // Prints: + * // [ + * // [Function: firstListener], + * // [Function: secondListener], + * // [Function: thirdListener] + * // ] + * // Helloooo! first listener + * // event with parameters 1, 2 in second listener + * // event with parameters 1, 2, 3, 4, 5 in third listener + * ``` + * @since v0.1.26 + */ + emit(eventName: Key, ...args: Args): boolean; + /** + * Returns the number of listeners listening for the event named `eventName`. + * If `listener` is provided, it will return how many times the listener is found + * in the list of the listeners of the event. + * @since v3.2.0 + * @param eventName The name of the event being listened for + * @param listener The event handler function + */ + listenerCount(eventName: Key, listener?: Listener2): number; + /** + * Adds the `listener` function to the _beginning_ of the listeners array for the + * event named `eventName`. No checks are made to see if the `listener` has + * already been added. Multiple calls passing the same combination of `eventName` + * and `listener` will result in the `listener` being added, and called, multiple times. + * + * ```js + * server.prependListener('connection', (stream) => { + * console.log('someone connected!'); + * }); + * ``` + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * @since v6.0.0 + * @param eventName The name of the event. + * @param listener The callback function + */ + prependListener(eventName: Key, listener: Listener1): this; + /** + * Adds a **one-time**`listener` function for the event named `eventName` to the _beginning_ of the listeners array. The next time `eventName` is triggered, this + * listener is removed, and then invoked. + * + * ```js + * server.prependOnceListener('connection', (stream) => { + * console.log('Ah, we have our first user!'); + * }); + * ``` + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * @since v6.0.0 + * @param eventName The name of the event. + * @param listener The callback function + */ + prependOnceListener(eventName: Key, listener: Listener1): this; + /** + * Returns an array listing the events for which the emitter has registered + * listeners. The values in the array are strings or `Symbol`s. + * + * ```js + * import { EventEmitter } from 'node:events'; + * + * const myEE = new EventEmitter(); + * myEE.on('foo', () => {}); + * myEE.on('bar', () => {}); + * + * const sym = Symbol('symbol'); + * myEE.on(sym, () => {}); + * + * console.log(myEE.eventNames()); + * // Prints: [ 'foo', 'bar', Symbol(symbol) ] + * ``` + * @since v6.0.0 + */ + eventNames(): Array<(string | symbol) & Key2>; + } + } + } + export = EventEmitter; +} +declare module "node:events" { + import events = require("events"); + export = events; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/fs.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/fs.d.ts new file mode 100644 index 00000000..d40515bf --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/fs.d.ts @@ -0,0 +1,4461 @@ +/** + * The `node:fs` module enables interacting with the file system in a + * way modeled on standard POSIX functions. + * + * To use the promise-based APIs: + * + * ```js + * import * as fs from 'node:fs/promises'; + * ``` + * + * To use the callback and sync APIs: + * + * ```js + * import * as fs from 'node:fs'; + * ``` + * + * All file system operations have synchronous, callback, and promise-based + * forms, and are accessible using both CommonJS syntax and ES6 Modules (ESM). + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/fs.js) + */ +declare module "fs" { + import { NonSharedBuffer } from "node:buffer"; + import * as stream from "node:stream"; + import { Abortable, EventEmitter } from "node:events"; + import { URL } from "node:url"; + import * as promises from "node:fs/promises"; + export { promises }; + /** + * Valid types for path values in "fs". + */ + export type PathLike = string | Buffer | URL; + export type PathOrFileDescriptor = PathLike | number; + export type TimeLike = string | number | Date; + export type NoParamCallback = (err: NodeJS.ErrnoException | null) => void; + export type BufferEncodingOption = + | "buffer" + | { + encoding: "buffer"; + }; + export interface ObjectEncodingOptions { + encoding?: BufferEncoding | null | undefined; + } + export type EncodingOption = ObjectEncodingOptions | BufferEncoding | undefined | null; + export type OpenMode = number | string; + export type Mode = number | string; + export interface StatsBase { + isFile(): boolean; + isDirectory(): boolean; + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isSymbolicLink(): boolean; + isFIFO(): boolean; + isSocket(): boolean; + dev: T; + ino: T; + mode: T; + nlink: T; + uid: T; + gid: T; + rdev: T; + size: T; + blksize: T; + blocks: T; + atimeMs: T; + mtimeMs: T; + ctimeMs: T; + birthtimeMs: T; + atime: Date; + mtime: Date; + ctime: Date; + birthtime: Date; + } + export interface Stats extends StatsBase {} + /** + * A `fs.Stats` object provides information about a file. + * + * Objects returned from {@link stat}, {@link lstat}, {@link fstat}, and + * their synchronous counterparts are of this type. + * If `bigint` in the `options` passed to those methods is true, the numeric values + * will be `bigint` instead of `number`, and the object will contain additional + * nanosecond-precision properties suffixed with `Ns`. `Stat` objects are not to be created directly using the `new` keyword. + * + * ```console + * Stats { + * dev: 2114, + * ino: 48064969, + * mode: 33188, + * nlink: 1, + * uid: 85, + * gid: 100, + * rdev: 0, + * size: 527, + * blksize: 4096, + * blocks: 8, + * atimeMs: 1318289051000.1, + * mtimeMs: 1318289051000.1, + * ctimeMs: 1318289051000.1, + * birthtimeMs: 1318289051000.1, + * atime: Mon, 10 Oct 2011 23:24:11 GMT, + * mtime: Mon, 10 Oct 2011 23:24:11 GMT, + * ctime: Mon, 10 Oct 2011 23:24:11 GMT, + * birthtime: Mon, 10 Oct 2011 23:24:11 GMT } + * ``` + * + * `bigint` version: + * + * ```console + * BigIntStats { + * dev: 2114n, + * ino: 48064969n, + * mode: 33188n, + * nlink: 1n, + * uid: 85n, + * gid: 100n, + * rdev: 0n, + * size: 527n, + * blksize: 4096n, + * blocks: 8n, + * atimeMs: 1318289051000n, + * mtimeMs: 1318289051000n, + * ctimeMs: 1318289051000n, + * birthtimeMs: 1318289051000n, + * atimeNs: 1318289051000000000n, + * mtimeNs: 1318289051000000000n, + * ctimeNs: 1318289051000000000n, + * birthtimeNs: 1318289051000000000n, + * atime: Mon, 10 Oct 2011 23:24:11 GMT, + * mtime: Mon, 10 Oct 2011 23:24:11 GMT, + * ctime: Mon, 10 Oct 2011 23:24:11 GMT, + * birthtime: Mon, 10 Oct 2011 23:24:11 GMT } + * ``` + * @since v0.1.21 + */ + export class Stats { + private constructor(); + } + export interface StatsFsBase { + /** Type of file system. */ + type: T; + /** Optimal transfer block size. */ + bsize: T; + /** Total data blocks in file system. */ + blocks: T; + /** Free blocks in file system. */ + bfree: T; + /** Available blocks for unprivileged users */ + bavail: T; + /** Total file nodes in file system. */ + files: T; + /** Free file nodes in file system. */ + ffree: T; + } + export interface StatsFs extends StatsFsBase {} + /** + * Provides information about a mounted file system. + * + * Objects returned from {@link statfs} and its synchronous counterpart are of + * this type. If `bigint` in the `options` passed to those methods is `true`, the + * numeric values will be `bigint` instead of `number`. + * + * ```console + * StatFs { + * type: 1397114950, + * bsize: 4096, + * blocks: 121938943, + * bfree: 61058895, + * bavail: 61058895, + * files: 999, + * ffree: 1000000 + * } + * ``` + * + * `bigint` version: + * + * ```console + * StatFs { + * type: 1397114950n, + * bsize: 4096n, + * blocks: 121938943n, + * bfree: 61058895n, + * bavail: 61058895n, + * files: 999n, + * ffree: 1000000n + * } + * ``` + * @since v19.6.0, v18.15.0 + */ + export class StatsFs {} + export interface BigIntStatsFs extends StatsFsBase {} + export interface StatFsOptions { + bigint?: boolean | undefined; + } + /** + * A representation of a directory entry, which can be a file or a subdirectory + * within the directory, as returned by reading from an `fs.Dir`. The + * directory entry is a combination of the file name and file type pairs. + * + * Additionally, when {@link readdir} or {@link readdirSync} is called with + * the `withFileTypes` option set to `true`, the resulting array is filled with `fs.Dirent` objects, rather than strings or `Buffer` s. + * @since v10.10.0 + */ + export class Dirent { + /** + * Returns `true` if the `fs.Dirent` object describes a regular file. + * @since v10.10.0 + */ + isFile(): boolean; + /** + * Returns `true` if the `fs.Dirent` object describes a file system + * directory. + * @since v10.10.0 + */ + isDirectory(): boolean; + /** + * Returns `true` if the `fs.Dirent` object describes a block device. + * @since v10.10.0 + */ + isBlockDevice(): boolean; + /** + * Returns `true` if the `fs.Dirent` object describes a character device. + * @since v10.10.0 + */ + isCharacterDevice(): boolean; + /** + * Returns `true` if the `fs.Dirent` object describes a symbolic link. + * @since v10.10.0 + */ + isSymbolicLink(): boolean; + /** + * Returns `true` if the `fs.Dirent` object describes a first-in-first-out + * (FIFO) pipe. + * @since v10.10.0 + */ + isFIFO(): boolean; + /** + * Returns `true` if the `fs.Dirent` object describes a socket. + * @since v10.10.0 + */ + isSocket(): boolean; + /** + * The file name that this `fs.Dirent` object refers to. The type of this + * value is determined by the `options.encoding` passed to {@link readdir} or {@link readdirSync}. + * @since v10.10.0 + */ + name: Name; + /** + * The path to the parent directory of the file this `fs.Dirent` object refers to. + * @since v20.12.0, v18.20.0 + */ + parentPath: string; + /** + * Alias for `dirent.parentPath`. + * @since v20.1.0 + * @deprecated Since v20.12.0 + */ + path: string; + } + /** + * A class representing a directory stream. + * + * Created by {@link opendir}, {@link opendirSync}, or `fsPromises.opendir()`. + * + * ```js + * import { opendir } from 'node:fs/promises'; + * + * try { + * const dir = await opendir('./'); + * for await (const dirent of dir) + * console.log(dirent.name); + * } catch (err) { + * console.error(err); + * } + * ``` + * + * When using the async iterator, the `fs.Dir` object will be automatically + * closed after the iterator exits. + * @since v12.12.0 + */ + export class Dir implements AsyncIterable { + /** + * The read-only path of this directory as was provided to {@link opendir},{@link opendirSync}, or `fsPromises.opendir()`. + * @since v12.12.0 + */ + readonly path: string; + /** + * Asynchronously iterates over the directory via `readdir(3)` until all entries have been read. + */ + [Symbol.asyncIterator](): NodeJS.AsyncIterator; + /** + * Asynchronously close the directory's underlying resource handle. + * Subsequent reads will result in errors. + * + * A promise is returned that will be fulfilled after the resource has been + * closed. + * @since v12.12.0 + */ + close(): Promise; + close(cb: NoParamCallback): void; + /** + * Synchronously close the directory's underlying resource handle. + * Subsequent reads will result in errors. + * @since v12.12.0 + */ + closeSync(): void; + /** + * Asynchronously read the next directory entry via [`readdir(3)`](http://man7.org/linux/man-pages/man3/readdir.3.html) as an `fs.Dirent`. + * + * A promise is returned that will be fulfilled with an `fs.Dirent`, or `null` if there are no more directory entries to read. + * + * Directory entries returned by this function are in no particular order as + * provided by the operating system's underlying directory mechanisms. + * Entries added or removed while iterating over the directory might not be + * included in the iteration results. + * @since v12.12.0 + * @return containing {fs.Dirent|null} + */ + read(): Promise; + read(cb: (err: NodeJS.ErrnoException | null, dirEnt: Dirent | null) => void): void; + /** + * Synchronously read the next directory entry as an `fs.Dirent`. See the + * POSIX [`readdir(3)`](http://man7.org/linux/man-pages/man3/readdir.3.html) documentation for more detail. + * + * If there are no more directory entries to read, `null` will be returned. + * + * Directory entries returned by this function are in no particular order as + * provided by the operating system's underlying directory mechanisms. + * Entries added or removed while iterating over the directory might not be + * included in the iteration results. + * @since v12.12.0 + */ + readSync(): Dirent | null; + /** + * Calls `dir.close()` if the directory handle is open, and returns a promise that + * fulfills when disposal is complete. + * @since v22.17.0 + * @experimental + */ + [Symbol.asyncDispose](): Promise; + /** + * Calls `dir.closeSync()` if the directory handle is open, and returns + * `undefined`. + * @since v22.17.0 + * @experimental + */ + [Symbol.dispose](): void; + } + /** + * Class: fs.StatWatcher + * @since v14.3.0, v12.20.0 + * Extends `EventEmitter` + * A successful call to {@link watchFile} method will return a new fs.StatWatcher object. + */ + export interface StatWatcher extends EventEmitter { + /** + * When called, requests that the Node.js event loop _not_ exit so long as the `fs.StatWatcher` is active. Calling `watcher.ref()` multiple times will have + * no effect. + * + * By default, all `fs.StatWatcher` objects are "ref'ed", making it normally + * unnecessary to call `watcher.ref()` unless `watcher.unref()` had been + * called previously. + * @since v14.3.0, v12.20.0 + */ + ref(): this; + /** + * When called, the active `fs.StatWatcher` object will not require the Node.js + * event loop to remain active. If there is no other activity keeping the + * event loop running, the process may exit before the `fs.StatWatcher` object's + * callback is invoked. Calling `watcher.unref()` multiple times will have + * no effect. + * @since v14.3.0, v12.20.0 + */ + unref(): this; + } + export interface FSWatcher extends EventEmitter { + /** + * Stop watching for changes on the given `fs.FSWatcher`. Once stopped, the `fs.FSWatcher` object is no longer usable. + * @since v0.5.8 + */ + close(): void; + /** + * When called, requests that the Node.js event loop _not_ exit so long as the `fs.FSWatcher` is active. Calling `watcher.ref()` multiple times will have + * no effect. + * + * By default, all `fs.FSWatcher` objects are "ref'ed", making it normally + * unnecessary to call `watcher.ref()` unless `watcher.unref()` had been + * called previously. + * @since v14.3.0, v12.20.0 + */ + ref(): this; + /** + * When called, the active `fs.FSWatcher` object will not require the Node.js + * event loop to remain active. If there is no other activity keeping the + * event loop running, the process may exit before the `fs.FSWatcher` object's + * callback is invoked. Calling `watcher.unref()` multiple times will have + * no effect. + * @since v14.3.0, v12.20.0 + */ + unref(): this; + /** + * events.EventEmitter + * 1. change + * 2. close + * 3. error + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "change", listener: (eventType: string, filename: string | NonSharedBuffer) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "error", listener: (error: Error) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "change", listener: (eventType: string, filename: string | NonSharedBuffer) => void): this; + on(event: "close", listener: () => void): this; + on(event: "error", listener: (error: Error) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "change", listener: (eventType: string, filename: string | NonSharedBuffer) => void): this; + once(event: "close", listener: () => void): this; + once(event: "error", listener: (error: Error) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener( + event: "change", + listener: (eventType: string, filename: string | NonSharedBuffer) => void, + ): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "error", listener: (error: Error) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener( + event: "change", + listener: (eventType: string, filename: string | NonSharedBuffer) => void, + ): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "error", listener: (error: Error) => void): this; + } + /** + * Instances of `fs.ReadStream` are created and returned using the {@link createReadStream} function. + * @since v0.1.93 + */ + export class ReadStream extends stream.Readable { + close(callback?: (err?: NodeJS.ErrnoException | null) => void): void; + /** + * The number of bytes that have been read so far. + * @since v6.4.0 + */ + bytesRead: number; + /** + * The path to the file the stream is reading from as specified in the first + * argument to `fs.createReadStream()`. If `path` is passed as a string, then`readStream.path` will be a string. If `path` is passed as a `Buffer`, then`readStream.path` will be a + * `Buffer`. If `fd` is specified, then`readStream.path` will be `undefined`. + * @since v0.1.93 + */ + path: string | Buffer; + /** + * This property is `true` if the underlying file has not been opened yet, + * i.e. before the `'ready'` event is emitted. + * @since v11.2.0, v10.16.0 + */ + pending: boolean; + /** + * events.EventEmitter + * 1. open + * 2. close + * 3. ready + */ + addListener(event: K, listener: ReadStreamEvents[K]): this; + on(event: K, listener: ReadStreamEvents[K]): this; + once(event: K, listener: ReadStreamEvents[K]): this; + prependListener(event: K, listener: ReadStreamEvents[K]): this; + prependOnceListener(event: K, listener: ReadStreamEvents[K]): this; + } + + /** + * The Keys are events of the ReadStream and the values are the functions that are called when the event is emitted. + */ + type ReadStreamEvents = { + close: () => void; + data: (chunk: Buffer | string) => void; + end: () => void; + error: (err: Error) => void; + open: (fd: number) => void; + pause: () => void; + readable: () => void; + ready: () => void; + resume: () => void; + } & CustomEvents; + + /** + * string & {} allows to allow any kind of strings for the event + * but still allows to have auto completion for the normal events. + */ + type CustomEvents = { [Key in string & {} | symbol]: (...args: any[]) => void }; + + /** + * The Keys are events of the WriteStream and the values are the functions that are called when the event is emitted. + */ + type WriteStreamEvents = { + close: () => void; + drain: () => void; + error: (err: Error) => void; + finish: () => void; + open: (fd: number) => void; + pipe: (src: stream.Readable) => void; + ready: () => void; + unpipe: (src: stream.Readable) => void; + } & CustomEvents; + /** + * * Extends `stream.Writable` + * + * Instances of `fs.WriteStream` are created and returned using the {@link createWriteStream} function. + * @since v0.1.93 + */ + export class WriteStream extends stream.Writable { + /** + * Closes `writeStream`. Optionally accepts a + * callback that will be executed once the `writeStream`is closed. + * @since v0.9.4 + */ + close(callback?: (err?: NodeJS.ErrnoException | null) => void): void; + /** + * The number of bytes written so far. Does not include data that is still queued + * for writing. + * @since v0.4.7 + */ + bytesWritten: number; + /** + * The path to the file the stream is writing to as specified in the first + * argument to {@link createWriteStream}. If `path` is passed as a string, then`writeStream.path` will be a string. If `path` is passed as a `Buffer`, then`writeStream.path` will be a + * `Buffer`. + * @since v0.1.93 + */ + path: string | Buffer; + /** + * This property is `true` if the underlying file has not been opened yet, + * i.e. before the `'ready'` event is emitted. + * @since v11.2.0 + */ + pending: boolean; + /** + * events.EventEmitter + * 1. open + * 2. close + * 3. ready + */ + addListener(event: K, listener: WriteStreamEvents[K]): this; + on(event: K, listener: WriteStreamEvents[K]): this; + once(event: K, listener: WriteStreamEvents[K]): this; + prependListener(event: K, listener: WriteStreamEvents[K]): this; + prependOnceListener(event: K, listener: WriteStreamEvents[K]): this; + } + /** + * Asynchronously rename file at `oldPath` to the pathname provided + * as `newPath`. In the case that `newPath` already exists, it will + * be overwritten. If there is a directory at `newPath`, an error will + * be raised instead. No arguments other than a possible exception are + * given to the completion callback. + * + * See also: [`rename(2)`](http://man7.org/linux/man-pages/man2/rename.2.html). + * + * ```js + * import { rename } from 'node:fs'; + * + * rename('oldFile.txt', 'newFile.txt', (err) => { + * if (err) throw err; + * console.log('Rename complete!'); + * }); + * ``` + * @since v0.0.2 + */ + export function rename(oldPath: PathLike, newPath: PathLike, callback: NoParamCallback): void; + export namespace rename { + /** + * Asynchronous rename(2) - Change the name or location of a file or directory. + * @param oldPath A path to a file. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + * @param newPath A path to a file. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + */ + function __promisify__(oldPath: PathLike, newPath: PathLike): Promise; + } + /** + * Renames the file from `oldPath` to `newPath`. Returns `undefined`. + * + * See the POSIX [`rename(2)`](http://man7.org/linux/man-pages/man2/rename.2.html) documentation for more details. + * @since v0.1.21 + */ + export function renameSync(oldPath: PathLike, newPath: PathLike): void; + /** + * Truncates the file. No arguments other than a possible exception are + * given to the completion callback. A file descriptor can also be passed as the + * first argument. In this case, `fs.ftruncate()` is called. + * + * ```js + * import { truncate } from 'node:fs'; + * // Assuming that 'path/file.txt' is a regular file. + * truncate('path/file.txt', (err) => { + * if (err) throw err; + * console.log('path/file.txt was truncated'); + * }); + * ``` + * + * Passing a file descriptor is deprecated and may result in an error being thrown + * in the future. + * + * See the POSIX [`truncate(2)`](http://man7.org/linux/man-pages/man2/truncate.2.html) documentation for more details. + * @since v0.8.6 + * @param [len=0] + */ + export function truncate(path: PathLike, len: number | undefined, callback: NoParamCallback): void; + /** + * Asynchronous truncate(2) - Truncate a file to a specified length. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export function truncate(path: PathLike, callback: NoParamCallback): void; + export namespace truncate { + /** + * Asynchronous truncate(2) - Truncate a file to a specified length. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param len If not specified, defaults to `0`. + */ + function __promisify__(path: PathLike, len?: number): Promise; + } + /** + * Truncates the file. Returns `undefined`. A file descriptor can also be + * passed as the first argument. In this case, `fs.ftruncateSync()` is called. + * + * Passing a file descriptor is deprecated and may result in an error being thrown + * in the future. + * @since v0.8.6 + * @param [len=0] + */ + export function truncateSync(path: PathLike, len?: number): void; + /** + * Truncates the file descriptor. No arguments other than a possible exception are + * given to the completion callback. + * + * See the POSIX [`ftruncate(2)`](http://man7.org/linux/man-pages/man2/ftruncate.2.html) documentation for more detail. + * + * If the file referred to by the file descriptor was larger than `len` bytes, only + * the first `len` bytes will be retained in the file. + * + * For example, the following program retains only the first four bytes of the + * file: + * + * ```js + * import { open, close, ftruncate } from 'node:fs'; + * + * function closeFd(fd) { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * + * open('temp.txt', 'r+', (err, fd) => { + * if (err) throw err; + * + * try { + * ftruncate(fd, 4, (err) => { + * closeFd(fd); + * if (err) throw err; + * }); + * } catch (err) { + * closeFd(fd); + * if (err) throw err; + * } + * }); + * ``` + * + * If the file previously was shorter than `len` bytes, it is extended, and the + * extended part is filled with null bytes (`'\0'`): + * + * If `len` is negative then `0` will be used. + * @since v0.8.6 + * @param [len=0] + */ + export function ftruncate(fd: number, len: number | undefined, callback: NoParamCallback): void; + /** + * Asynchronous ftruncate(2) - Truncate a file to a specified length. + * @param fd A file descriptor. + */ + export function ftruncate(fd: number, callback: NoParamCallback): void; + export namespace ftruncate { + /** + * Asynchronous ftruncate(2) - Truncate a file to a specified length. + * @param fd A file descriptor. + * @param len If not specified, defaults to `0`. + */ + function __promisify__(fd: number, len?: number): Promise; + } + /** + * Truncates the file descriptor. Returns `undefined`. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link ftruncate}. + * @since v0.8.6 + * @param [len=0] + */ + export function ftruncateSync(fd: number, len?: number): void; + /** + * Asynchronously changes owner and group of a file. No arguments other than a + * possible exception are given to the completion callback. + * + * See the POSIX [`chown(2)`](http://man7.org/linux/man-pages/man2/chown.2.html) documentation for more detail. + * @since v0.1.97 + */ + export function chown(path: PathLike, uid: number, gid: number, callback: NoParamCallback): void; + export namespace chown { + /** + * Asynchronous chown(2) - Change ownership of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + function __promisify__(path: PathLike, uid: number, gid: number): Promise; + } + /** + * Synchronously changes owner and group of a file. Returns `undefined`. + * This is the synchronous version of {@link chown}. + * + * See the POSIX [`chown(2)`](http://man7.org/linux/man-pages/man2/chown.2.html) documentation for more detail. + * @since v0.1.97 + */ + export function chownSync(path: PathLike, uid: number, gid: number): void; + /** + * Sets the owner of the file. No arguments other than a possible exception are + * given to the completion callback. + * + * See the POSIX [`fchown(2)`](http://man7.org/linux/man-pages/man2/fchown.2.html) documentation for more detail. + * @since v0.4.7 + */ + export function fchown(fd: number, uid: number, gid: number, callback: NoParamCallback): void; + export namespace fchown { + /** + * Asynchronous fchown(2) - Change ownership of a file. + * @param fd A file descriptor. + */ + function __promisify__(fd: number, uid: number, gid: number): Promise; + } + /** + * Sets the owner of the file. Returns `undefined`. + * + * See the POSIX [`fchown(2)`](http://man7.org/linux/man-pages/man2/fchown.2.html) documentation for more detail. + * @since v0.4.7 + * @param uid The file's new owner's user id. + * @param gid The file's new group's group id. + */ + export function fchownSync(fd: number, uid: number, gid: number): void; + /** + * Set the owner of the symbolic link. No arguments other than a possible + * exception are given to the completion callback. + * + * See the POSIX [`lchown(2)`](http://man7.org/linux/man-pages/man2/lchown.2.html) documentation for more detail. + */ + export function lchown(path: PathLike, uid: number, gid: number, callback: NoParamCallback): void; + export namespace lchown { + /** + * Asynchronous lchown(2) - Change ownership of a file. Does not dereference symbolic links. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + function __promisify__(path: PathLike, uid: number, gid: number): Promise; + } + /** + * Set the owner for the path. Returns `undefined`. + * + * See the POSIX [`lchown(2)`](http://man7.org/linux/man-pages/man2/lchown.2.html) documentation for more details. + * @param uid The file's new owner's user id. + * @param gid The file's new group's group id. + */ + export function lchownSync(path: PathLike, uid: number, gid: number): void; + /** + * Changes the access and modification times of a file in the same way as {@link utimes}, with the difference that if the path refers to a symbolic + * link, then the link is not dereferenced: instead, the timestamps of the + * symbolic link itself are changed. + * + * No arguments other than a possible exception are given to the completion + * callback. + * @since v14.5.0, v12.19.0 + */ + export function lutimes(path: PathLike, atime: TimeLike, mtime: TimeLike, callback: NoParamCallback): void; + export namespace lutimes { + /** + * Changes the access and modification times of a file in the same way as `fsPromises.utimes()`, + * with the difference that if the path refers to a symbolic link, then the link is not + * dereferenced: instead, the timestamps of the symbolic link itself are changed. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param atime The last access time. If a string is provided, it will be coerced to number. + * @param mtime The last modified time. If a string is provided, it will be coerced to number. + */ + function __promisify__(path: PathLike, atime: TimeLike, mtime: TimeLike): Promise; + } + /** + * Change the file system timestamps of the symbolic link referenced by `path`. + * Returns `undefined`, or throws an exception when parameters are incorrect or + * the operation fails. This is the synchronous version of {@link lutimes}. + * @since v14.5.0, v12.19.0 + */ + export function lutimesSync(path: PathLike, atime: TimeLike, mtime: TimeLike): void; + /** + * Asynchronously changes the permissions of a file. No arguments other than a + * possible exception are given to the completion callback. + * + * See the POSIX [`chmod(2)`](http://man7.org/linux/man-pages/man2/chmod.2.html) documentation for more detail. + * + * ```js + * import { chmod } from 'node:fs'; + * + * chmod('my_file.txt', 0o775, (err) => { + * if (err) throw err; + * console.log('The permissions for file "my_file.txt" have been changed!'); + * }); + * ``` + * @since v0.1.30 + */ + export function chmod(path: PathLike, mode: Mode, callback: NoParamCallback): void; + export namespace chmod { + /** + * Asynchronous chmod(2) - Change permissions of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param mode A file mode. If a string is passed, it is parsed as an octal integer. + */ + function __promisify__(path: PathLike, mode: Mode): Promise; + } + /** + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link chmod}. + * + * See the POSIX [`chmod(2)`](http://man7.org/linux/man-pages/man2/chmod.2.html) documentation for more detail. + * @since v0.6.7 + */ + export function chmodSync(path: PathLike, mode: Mode): void; + /** + * Sets the permissions on the file. No arguments other than a possible exception + * are given to the completion callback. + * + * See the POSIX [`fchmod(2)`](http://man7.org/linux/man-pages/man2/fchmod.2.html) documentation for more detail. + * @since v0.4.7 + */ + export function fchmod(fd: number, mode: Mode, callback: NoParamCallback): void; + export namespace fchmod { + /** + * Asynchronous fchmod(2) - Change permissions of a file. + * @param fd A file descriptor. + * @param mode A file mode. If a string is passed, it is parsed as an octal integer. + */ + function __promisify__(fd: number, mode: Mode): Promise; + } + /** + * Sets the permissions on the file. Returns `undefined`. + * + * See the POSIX [`fchmod(2)`](http://man7.org/linux/man-pages/man2/fchmod.2.html) documentation for more detail. + * @since v0.4.7 + */ + export function fchmodSync(fd: number, mode: Mode): void; + /** + * Changes the permissions on a symbolic link. No arguments other than a possible + * exception are given to the completion callback. + * + * This method is only implemented on macOS. + * + * See the POSIX [`lchmod(2)`](https://www.freebsd.org/cgi/man.cgi?query=lchmod&sektion=2) documentation for more detail. + * @deprecated Since v0.4.7 + */ + export function lchmod(path: PathLike, mode: Mode, callback: NoParamCallback): void; + /** @deprecated */ + export namespace lchmod { + /** + * Asynchronous lchmod(2) - Change permissions of a file. Does not dereference symbolic links. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param mode A file mode. If a string is passed, it is parsed as an octal integer. + */ + function __promisify__(path: PathLike, mode: Mode): Promise; + } + /** + * Changes the permissions on a symbolic link. Returns `undefined`. + * + * This method is only implemented on macOS. + * + * See the POSIX [`lchmod(2)`](https://www.freebsd.org/cgi/man.cgi?query=lchmod&sektion=2) documentation for more detail. + * @deprecated Since v0.4.7 + */ + export function lchmodSync(path: PathLike, mode: Mode): void; + /** + * Asynchronous [`stat(2)`](http://man7.org/linux/man-pages/man2/stat.2.html). The callback gets two arguments `(err, stats)` where`stats` is an `fs.Stats` object. + * + * In case of an error, the `err.code` will be one of `Common System Errors`. + * + * {@link stat} follows symbolic links. Use {@link lstat} to look at the + * links themselves. + * + * Using `fs.stat()` to check for the existence of a file before calling`fs.open()`, `fs.readFile()`, or `fs.writeFile()` is not recommended. + * Instead, user code should open/read/write the file directly and handle the + * error raised if the file is not available. + * + * To check if a file exists without manipulating it afterwards, {@link access} is recommended. + * + * For example, given the following directory structure: + * + * ```text + * - txtDir + * -- file.txt + * - app.js + * ``` + * + * The next program will check for the stats of the given paths: + * + * ```js + * import { stat } from 'node:fs'; + * + * const pathsToCheck = ['./txtDir', './txtDir/file.txt']; + * + * for (let i = 0; i < pathsToCheck.length; i++) { + * stat(pathsToCheck[i], (err, stats) => { + * console.log(stats.isDirectory()); + * console.log(stats); + * }); + * } + * ``` + * + * The resulting output will resemble: + * + * ```console + * true + * Stats { + * dev: 16777220, + * mode: 16877, + * nlink: 3, + * uid: 501, + * gid: 20, + * rdev: 0, + * blksize: 4096, + * ino: 14214262, + * size: 96, + * blocks: 0, + * atimeMs: 1561174653071.963, + * mtimeMs: 1561174614583.3518, + * ctimeMs: 1561174626623.5366, + * birthtimeMs: 1561174126937.2893, + * atime: 2019-06-22T03:37:33.072Z, + * mtime: 2019-06-22T03:36:54.583Z, + * ctime: 2019-06-22T03:37:06.624Z, + * birthtime: 2019-06-22T03:28:46.937Z + * } + * false + * Stats { + * dev: 16777220, + * mode: 33188, + * nlink: 1, + * uid: 501, + * gid: 20, + * rdev: 0, + * blksize: 4096, + * ino: 14214074, + * size: 8, + * blocks: 8, + * atimeMs: 1561174616618.8555, + * mtimeMs: 1561174614584, + * ctimeMs: 1561174614583.8145, + * birthtimeMs: 1561174007710.7478, + * atime: 2019-06-22T03:36:56.619Z, + * mtime: 2019-06-22T03:36:54.584Z, + * ctime: 2019-06-22T03:36:54.584Z, + * birthtime: 2019-06-22T03:26:47.711Z + * } + * ``` + * @since v0.0.2 + */ + export function stat(path: PathLike, callback: (err: NodeJS.ErrnoException | null, stats: Stats) => void): void; + export function stat( + path: PathLike, + options: + | (StatOptions & { + bigint?: false | undefined; + }) + | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: Stats) => void, + ): void; + export function stat( + path: PathLike, + options: StatOptions & { + bigint: true; + }, + callback: (err: NodeJS.ErrnoException | null, stats: BigIntStats) => void, + ): void; + export function stat( + path: PathLike, + options: StatOptions | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: Stats | BigIntStats) => void, + ): void; + export namespace stat { + /** + * Asynchronous stat(2) - Get file status. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + function __promisify__( + path: PathLike, + options?: StatOptions & { + bigint?: false | undefined; + }, + ): Promise; + function __promisify__( + path: PathLike, + options: StatOptions & { + bigint: true; + }, + ): Promise; + function __promisify__(path: PathLike, options?: StatOptions): Promise; + } + export interface StatSyncFn extends Function { + (path: PathLike, options?: undefined): Stats; + ( + path: PathLike, + options?: StatSyncOptions & { + bigint?: false | undefined; + throwIfNoEntry: false; + }, + ): Stats | undefined; + ( + path: PathLike, + options: StatSyncOptions & { + bigint: true; + throwIfNoEntry: false; + }, + ): BigIntStats | undefined; + ( + path: PathLike, + options?: StatSyncOptions & { + bigint?: false | undefined; + }, + ): Stats; + ( + path: PathLike, + options: StatSyncOptions & { + bigint: true; + }, + ): BigIntStats; + ( + path: PathLike, + options: StatSyncOptions & { + bigint: boolean; + throwIfNoEntry?: false | undefined; + }, + ): Stats | BigIntStats; + (path: PathLike, options?: StatSyncOptions): Stats | BigIntStats | undefined; + } + /** + * Synchronous stat(2) - Get file status. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export const statSync: StatSyncFn; + /** + * Invokes the callback with the `fs.Stats` for the file descriptor. + * + * See the POSIX [`fstat(2)`](http://man7.org/linux/man-pages/man2/fstat.2.html) documentation for more detail. + * @since v0.1.95 + */ + export function fstat(fd: number, callback: (err: NodeJS.ErrnoException | null, stats: Stats) => void): void; + export function fstat( + fd: number, + options: + | (StatOptions & { + bigint?: false | undefined; + }) + | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: Stats) => void, + ): void; + export function fstat( + fd: number, + options: StatOptions & { + bigint: true; + }, + callback: (err: NodeJS.ErrnoException | null, stats: BigIntStats) => void, + ): void; + export function fstat( + fd: number, + options: StatOptions | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: Stats | BigIntStats) => void, + ): void; + export namespace fstat { + /** + * Asynchronous fstat(2) - Get file status. + * @param fd A file descriptor. + */ + function __promisify__( + fd: number, + options?: StatOptions & { + bigint?: false | undefined; + }, + ): Promise; + function __promisify__( + fd: number, + options: StatOptions & { + bigint: true; + }, + ): Promise; + function __promisify__(fd: number, options?: StatOptions): Promise; + } + /** + * Retrieves the `fs.Stats` for the file descriptor. + * + * See the POSIX [`fstat(2)`](http://man7.org/linux/man-pages/man2/fstat.2.html) documentation for more detail. + * @since v0.1.95 + */ + export function fstatSync( + fd: number, + options?: StatOptions & { + bigint?: false | undefined; + }, + ): Stats; + export function fstatSync( + fd: number, + options: StatOptions & { + bigint: true; + }, + ): BigIntStats; + export function fstatSync(fd: number, options?: StatOptions): Stats | BigIntStats; + /** + * Retrieves the `fs.Stats` for the symbolic link referred to by the path. + * The callback gets two arguments `(err, stats)` where `stats` is a `fs.Stats` object. `lstat()` is identical to `stat()`, except that if `path` is a symbolic + * link, then the link itself is stat-ed, not the file that it refers to. + * + * See the POSIX [`lstat(2)`](http://man7.org/linux/man-pages/man2/lstat.2.html) documentation for more details. + * @since v0.1.30 + */ + export function lstat(path: PathLike, callback: (err: NodeJS.ErrnoException | null, stats: Stats) => void): void; + export function lstat( + path: PathLike, + options: + | (StatOptions & { + bigint?: false | undefined; + }) + | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: Stats) => void, + ): void; + export function lstat( + path: PathLike, + options: StatOptions & { + bigint: true; + }, + callback: (err: NodeJS.ErrnoException | null, stats: BigIntStats) => void, + ): void; + export function lstat( + path: PathLike, + options: StatOptions | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: Stats | BigIntStats) => void, + ): void; + export namespace lstat { + /** + * Asynchronous lstat(2) - Get file status. Does not dereference symbolic links. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + function __promisify__( + path: PathLike, + options?: StatOptions & { + bigint?: false | undefined; + }, + ): Promise; + function __promisify__( + path: PathLike, + options: StatOptions & { + bigint: true; + }, + ): Promise; + function __promisify__(path: PathLike, options?: StatOptions): Promise; + } + /** + * Asynchronous [`statfs(2)`](http://man7.org/linux/man-pages/man2/statfs.2.html). Returns information about the mounted file system which + * contains `path`. The callback gets two arguments `(err, stats)` where `stats`is an `fs.StatFs` object. + * + * In case of an error, the `err.code` will be one of `Common System Errors`. + * @since v19.6.0, v18.15.0 + * @param path A path to an existing file or directory on the file system to be queried. + */ + export function statfs(path: PathLike, callback: (err: NodeJS.ErrnoException | null, stats: StatsFs) => void): void; + export function statfs( + path: PathLike, + options: + | (StatFsOptions & { + bigint?: false | undefined; + }) + | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: StatsFs) => void, + ): void; + export function statfs( + path: PathLike, + options: StatFsOptions & { + bigint: true; + }, + callback: (err: NodeJS.ErrnoException | null, stats: BigIntStatsFs) => void, + ): void; + export function statfs( + path: PathLike, + options: StatFsOptions | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: StatsFs | BigIntStatsFs) => void, + ): void; + export namespace statfs { + /** + * Asynchronous statfs(2) - Returns information about the mounted file system which contains path. The callback gets two arguments (err, stats) where stats is an object. + * @param path A path to an existing file or directory on the file system to be queried. + */ + function __promisify__( + path: PathLike, + options?: StatFsOptions & { + bigint?: false | undefined; + }, + ): Promise; + function __promisify__( + path: PathLike, + options: StatFsOptions & { + bigint: true; + }, + ): Promise; + function __promisify__(path: PathLike, options?: StatFsOptions): Promise; + } + /** + * Synchronous [`statfs(2)`](http://man7.org/linux/man-pages/man2/statfs.2.html). Returns information about the mounted file system which + * contains `path`. + * + * In case of an error, the `err.code` will be one of `Common System Errors`. + * @since v19.6.0, v18.15.0 + * @param path A path to an existing file or directory on the file system to be queried. + */ + export function statfsSync( + path: PathLike, + options?: StatFsOptions & { + bigint?: false | undefined; + }, + ): StatsFs; + export function statfsSync( + path: PathLike, + options: StatFsOptions & { + bigint: true; + }, + ): BigIntStatsFs; + export function statfsSync(path: PathLike, options?: StatFsOptions): StatsFs | BigIntStatsFs; + /** + * Synchronous lstat(2) - Get file status. Does not dereference symbolic links. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export const lstatSync: StatSyncFn; + /** + * Creates a new link from the `existingPath` to the `newPath`. See the POSIX [`link(2)`](http://man7.org/linux/man-pages/man2/link.2.html) documentation for more detail. No arguments other than + * a possible + * exception are given to the completion callback. + * @since v0.1.31 + */ + export function link(existingPath: PathLike, newPath: PathLike, callback: NoParamCallback): void; + export namespace link { + /** + * Asynchronous link(2) - Create a new link (also known as a hard link) to an existing file. + * @param existingPath A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param newPath A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + function __promisify__(existingPath: PathLike, newPath: PathLike): Promise; + } + /** + * Creates a new link from the `existingPath` to the `newPath`. See the POSIX [`link(2)`](http://man7.org/linux/man-pages/man2/link.2.html) documentation for more detail. Returns `undefined`. + * @since v0.1.31 + */ + export function linkSync(existingPath: PathLike, newPath: PathLike): void; + /** + * Creates the link called `path` pointing to `target`. No arguments other than a + * possible exception are given to the completion callback. + * + * See the POSIX [`symlink(2)`](http://man7.org/linux/man-pages/man2/symlink.2.html) documentation for more details. + * + * The `type` argument is only available on Windows and ignored on other platforms. + * It can be set to `'dir'`, `'file'`, or `'junction'`. If the `type` argument is + * not a string, Node.js will autodetect `target` type and use `'file'` or `'dir'`. + * If the `target` does not exist, `'file'` will be used. Windows junction points + * require the destination path to be absolute. When using `'junction'`, the`target` argument will automatically be normalized to absolute path. Junction + * points on NTFS volumes can only point to directories. + * + * Relative targets are relative to the link's parent directory. + * + * ```js + * import { symlink } from 'node:fs'; + * + * symlink('./mew', './mewtwo', callback); + * ``` + * + * The above example creates a symbolic link `mewtwo` which points to `mew` in the + * same directory: + * + * ```bash + * $ tree . + * . + * ├── mew + * └── mewtwo -> ./mew + * ``` + * @since v0.1.31 + * @param [type='null'] + */ + export function symlink( + target: PathLike, + path: PathLike, + type: symlink.Type | undefined | null, + callback: NoParamCallback, + ): void; + /** + * Asynchronous symlink(2) - Create a new symbolic link to an existing file. + * @param target A path to an existing file. If a URL is provided, it must use the `file:` protocol. + * @param path A path to the new symlink. If a URL is provided, it must use the `file:` protocol. + */ + export function symlink(target: PathLike, path: PathLike, callback: NoParamCallback): void; + export namespace symlink { + /** + * Asynchronous symlink(2) - Create a new symbolic link to an existing file. + * @param target A path to an existing file. If a URL is provided, it must use the `file:` protocol. + * @param path A path to the new symlink. If a URL is provided, it must use the `file:` protocol. + * @param type May be set to `'dir'`, `'file'`, or `'junction'` (default is `'file'`) and is only available on Windows (ignored on other platforms). + * When using `'junction'`, the `target` argument will automatically be normalized to an absolute path. + */ + function __promisify__(target: PathLike, path: PathLike, type?: string | null): Promise; + type Type = "dir" | "file" | "junction"; + } + /** + * Returns `undefined`. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link symlink}. + * @since v0.1.31 + * @param [type='null'] + */ + export function symlinkSync(target: PathLike, path: PathLike, type?: symlink.Type | null): void; + /** + * Reads the contents of the symbolic link referred to by `path`. The callback gets + * two arguments `(err, linkString)`. + * + * See the POSIX [`readlink(2)`](http://man7.org/linux/man-pages/man2/readlink.2.html) documentation for more details. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the link path passed to the callback. If the `encoding` is set to `'buffer'`, + * the link path returned will be passed as a `Buffer` object. + * @since v0.1.31 + */ + export function readlink( + path: PathLike, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, linkString: string) => void, + ): void; + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readlink( + path: PathLike, + options: BufferEncodingOption, + callback: (err: NodeJS.ErrnoException | null, linkString: NonSharedBuffer) => void, + ): void; + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readlink( + path: PathLike, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, linkString: string | NonSharedBuffer) => void, + ): void; + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export function readlink( + path: PathLike, + callback: (err: NodeJS.ErrnoException | null, linkString: string) => void, + ): void; + export namespace readlink { + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(path: PathLike, options?: EncodingOption): Promise; + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(path: PathLike, options: BufferEncodingOption): Promise; + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(path: PathLike, options?: EncodingOption): Promise; + } + /** + * Returns the symbolic link's string value. + * + * See the POSIX [`readlink(2)`](http://man7.org/linux/man-pages/man2/readlink.2.html) documentation for more details. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the link path returned. If the `encoding` is set to `'buffer'`, + * the link path returned will be passed as a `Buffer` object. + * @since v0.1.31 + */ + export function readlinkSync(path: PathLike, options?: EncodingOption): string; + /** + * Synchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readlinkSync(path: PathLike, options: BufferEncodingOption): NonSharedBuffer; + /** + * Synchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readlinkSync(path: PathLike, options?: EncodingOption): string | NonSharedBuffer; + /** + * Asynchronously computes the canonical pathname by resolving `.`, `..`, and + * symbolic links. + * + * A canonical pathname is not necessarily unique. Hard links and bind mounts can + * expose a file system entity through many pathnames. + * + * This function behaves like [`realpath(3)`](http://man7.org/linux/man-pages/man3/realpath.3.html), with some exceptions: + * + * 1. No case conversion is performed on case-insensitive file systems. + * 2. The maximum number of symbolic links is platform-independent and generally + * (much) higher than what the native [`realpath(3)`](http://man7.org/linux/man-pages/man3/realpath.3.html) implementation supports. + * + * The `callback` gets two arguments `(err, resolvedPath)`. May use `process.cwd` to resolve relative paths. + * + * Only paths that can be converted to UTF8 strings are supported. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the path passed to the callback. If the `encoding` is set to `'buffer'`, + * the path returned will be passed as a `Buffer` object. + * + * If `path` resolves to a socket or a pipe, the function will return a system + * dependent name for that object. + * @since v0.1.31 + */ + export function realpath( + path: PathLike, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: string) => void, + ): void; + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function realpath( + path: PathLike, + options: BufferEncodingOption, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: NonSharedBuffer) => void, + ): void; + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function realpath( + path: PathLike, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: string | NonSharedBuffer) => void, + ): void; + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export function realpath( + path: PathLike, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: string) => void, + ): void; + export namespace realpath { + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(path: PathLike, options?: EncodingOption): Promise; + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(path: PathLike, options: BufferEncodingOption): Promise; + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(path: PathLike, options?: EncodingOption): Promise; + /** + * Asynchronous [`realpath(3)`](http://man7.org/linux/man-pages/man3/realpath.3.html). + * + * The `callback` gets two arguments `(err, resolvedPath)`. + * + * Only paths that can be converted to UTF8 strings are supported. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the path passed to the callback. If the `encoding` is set to `'buffer'`, + * the path returned will be passed as a `Buffer` object. + * + * On Linux, when Node.js is linked against musl libc, the procfs file system must + * be mounted on `/proc` in order for this function to work. Glibc does not have + * this restriction. + * @since v9.2.0 + */ + function native( + path: PathLike, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: string) => void, + ): void; + function native( + path: PathLike, + options: BufferEncodingOption, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: NonSharedBuffer) => void, + ): void; + function native( + path: PathLike, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: string | NonSharedBuffer) => void, + ): void; + function native( + path: PathLike, + callback: (err: NodeJS.ErrnoException | null, resolvedPath: string) => void, + ): void; + } + /** + * Returns the resolved pathname. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link realpath}. + * @since v0.1.31 + */ + export function realpathSync(path: PathLike, options?: EncodingOption): string; + /** + * Synchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function realpathSync(path: PathLike, options: BufferEncodingOption): NonSharedBuffer; + /** + * Synchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function realpathSync(path: PathLike, options?: EncodingOption): string | NonSharedBuffer; + export namespace realpathSync { + function native(path: PathLike, options?: EncodingOption): string; + function native(path: PathLike, options: BufferEncodingOption): NonSharedBuffer; + function native(path: PathLike, options?: EncodingOption): string | NonSharedBuffer; + } + /** + * Asynchronously removes a file or symbolic link. No arguments other than a + * possible exception are given to the completion callback. + * + * ```js + * import { unlink } from 'node:fs'; + * // Assuming that 'path/file.txt' is a regular file. + * unlink('path/file.txt', (err) => { + * if (err) throw err; + * console.log('path/file.txt was deleted'); + * }); + * ``` + * + * `fs.unlink()` will not work on a directory, empty or otherwise. To remove a + * directory, use {@link rmdir}. + * + * See the POSIX [`unlink(2)`](http://man7.org/linux/man-pages/man2/unlink.2.html) documentation for more details. + * @since v0.0.2 + */ + export function unlink(path: PathLike, callback: NoParamCallback): void; + export namespace unlink { + /** + * Asynchronous unlink(2) - delete a name and possibly the file it refers to. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + function __promisify__(path: PathLike): Promise; + } + /** + * Synchronous [`unlink(2)`](http://man7.org/linux/man-pages/man2/unlink.2.html). Returns `undefined`. + * @since v0.1.21 + */ + export function unlinkSync(path: PathLike): void; + export interface RmDirOptions { + /** + * If an `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY`, or + * `EPERM` error is encountered, Node.js will retry the operation with a linear + * backoff wait of `retryDelay` ms longer on each try. This option represents the + * number of retries. This option is ignored if the `recursive` option is not + * `true`. + * @default 0 + */ + maxRetries?: number | undefined; + /** + * @deprecated since v14.14.0 In future versions of Node.js and will trigger a warning + * `fs.rmdir(path, { recursive: true })` will throw if `path` does not exist or is a file. + * Use `fs.rm(path, { recursive: true, force: true })` instead. + * + * If `true`, perform a recursive directory removal. In + * recursive mode, operations are retried on failure. + * @default false + */ + recursive?: boolean | undefined; + /** + * The amount of time in milliseconds to wait between retries. + * This option is ignored if the `recursive` option is not `true`. + * @default 100 + */ + retryDelay?: number | undefined; + } + /** + * Asynchronous [`rmdir(2)`](http://man7.org/linux/man-pages/man2/rmdir.2.html). No arguments other than a possible exception are given + * to the completion callback. + * + * Using `fs.rmdir()` on a file (not a directory) results in an `ENOENT` error on + * Windows and an `ENOTDIR` error on POSIX. + * + * To get a behavior similar to the `rm -rf` Unix command, use {@link rm} with options `{ recursive: true, force: true }`. + * @since v0.0.2 + */ + export function rmdir(path: PathLike, callback: NoParamCallback): void; + export function rmdir(path: PathLike, options: RmDirOptions, callback: NoParamCallback): void; + export namespace rmdir { + /** + * Asynchronous rmdir(2) - delete a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + function __promisify__(path: PathLike, options?: RmDirOptions): Promise; + } + /** + * Synchronous [`rmdir(2)`](http://man7.org/linux/man-pages/man2/rmdir.2.html). Returns `undefined`. + * + * Using `fs.rmdirSync()` on a file (not a directory) results in an `ENOENT` error + * on Windows and an `ENOTDIR` error on POSIX. + * + * To get a behavior similar to the `rm -rf` Unix command, use {@link rmSync} with options `{ recursive: true, force: true }`. + * @since v0.1.21 + */ + export function rmdirSync(path: PathLike, options?: RmDirOptions): void; + export interface RmOptions { + /** + * When `true`, exceptions will be ignored if `path` does not exist. + * @default false + */ + force?: boolean | undefined; + /** + * If an `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY`, or + * `EPERM` error is encountered, Node.js will retry the operation with a linear + * backoff wait of `retryDelay` ms longer on each try. This option represents the + * number of retries. This option is ignored if the `recursive` option is not + * `true`. + * @default 0 + */ + maxRetries?: number | undefined; + /** + * If `true`, perform a recursive directory removal. In + * recursive mode, operations are retried on failure. + * @default false + */ + recursive?: boolean | undefined; + /** + * The amount of time in milliseconds to wait between retries. + * This option is ignored if the `recursive` option is not `true`. + * @default 100 + */ + retryDelay?: number | undefined; + } + /** + * Asynchronously removes files and directories (modeled on the standard POSIX `rm` utility). No arguments other than a possible exception are given to the + * completion callback. + * @since v14.14.0 + */ + export function rm(path: PathLike, callback: NoParamCallback): void; + export function rm(path: PathLike, options: RmOptions, callback: NoParamCallback): void; + export namespace rm { + /** + * Asynchronously removes files and directories (modeled on the standard POSIX `rm` utility). + */ + function __promisify__(path: PathLike, options?: RmOptions): Promise; + } + /** + * Synchronously removes files and directories (modeled on the standard POSIX `rm` utility). Returns `undefined`. + * @since v14.14.0 + */ + export function rmSync(path: PathLike, options?: RmOptions): void; + export interface MakeDirectoryOptions { + /** + * Indicates whether parent folders should be created. + * If a folder was created, the path to the first created folder will be returned. + * @default false + */ + recursive?: boolean | undefined; + /** + * A file mode. If a string is passed, it is parsed as an octal integer. If not specified + * @default 0o777 + */ + mode?: Mode | undefined; + } + /** + * Asynchronously creates a directory. + * + * The callback is given a possible exception and, if `recursive` is `true`, the + * first directory path created, `(err[, path])`.`path` can still be `undefined` when `recursive` is `true`, if no directory was + * created (for instance, if it was previously created). + * + * The optional `options` argument can be an integer specifying `mode` (permission + * and sticky bits), or an object with a `mode` property and a `recursive` property indicating whether parent directories should be created. Calling `fs.mkdir()` when `path` is a directory that + * exists results in an error only + * when `recursive` is false. If `recursive` is false and the directory exists, + * an `EEXIST` error occurs. + * + * ```js + * import { mkdir } from 'node:fs'; + * + * // Create ./tmp/a/apple, regardless of whether ./tmp and ./tmp/a exist. + * mkdir('./tmp/a/apple', { recursive: true }, (err) => { + * if (err) throw err; + * }); + * ``` + * + * On Windows, using `fs.mkdir()` on the root directory even with recursion will + * result in an error: + * + * ```js + * import { mkdir } from 'node:fs'; + * + * mkdir('/', { recursive: true }, (err) => { + * // => [Error: EPERM: operation not permitted, mkdir 'C:\'] + * }); + * ``` + * + * See the POSIX [`mkdir(2)`](http://man7.org/linux/man-pages/man2/mkdir.2.html) documentation for more details. + * @since v0.1.8 + */ + export function mkdir( + path: PathLike, + options: MakeDirectoryOptions & { + recursive: true; + }, + callback: (err: NodeJS.ErrnoException | null, path?: string) => void, + ): void; + /** + * Asynchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + export function mkdir( + path: PathLike, + options: + | Mode + | (MakeDirectoryOptions & { + recursive?: false | undefined; + }) + | null + | undefined, + callback: NoParamCallback, + ): void; + /** + * Asynchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + export function mkdir( + path: PathLike, + options: Mode | MakeDirectoryOptions | null | undefined, + callback: (err: NodeJS.ErrnoException | null, path?: string) => void, + ): void; + /** + * Asynchronous mkdir(2) - create a directory with a mode of `0o777`. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export function mkdir(path: PathLike, callback: NoParamCallback): void; + export namespace mkdir { + /** + * Asynchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + function __promisify__( + path: PathLike, + options: MakeDirectoryOptions & { + recursive: true; + }, + ): Promise; + /** + * Asynchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + function __promisify__( + path: PathLike, + options?: + | Mode + | (MakeDirectoryOptions & { + recursive?: false | undefined; + }) + | null, + ): Promise; + /** + * Asynchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + function __promisify__( + path: PathLike, + options?: Mode | MakeDirectoryOptions | null, + ): Promise; + } + /** + * Synchronously creates a directory. Returns `undefined`, or if `recursive` is `true`, the first directory path created. + * This is the synchronous version of {@link mkdir}. + * + * See the POSIX [`mkdir(2)`](http://man7.org/linux/man-pages/man2/mkdir.2.html) documentation for more details. + * @since v0.1.21 + */ + export function mkdirSync( + path: PathLike, + options: MakeDirectoryOptions & { + recursive: true; + }, + ): string | undefined; + /** + * Synchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + export function mkdirSync( + path: PathLike, + options?: + | Mode + | (MakeDirectoryOptions & { + recursive?: false | undefined; + }) + | null, + ): void; + /** + * Synchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + export function mkdirSync(path: PathLike, options?: Mode | MakeDirectoryOptions | null): string | undefined; + /** + * Creates a unique temporary directory. + * + * Generates six random characters to be appended behind a required `prefix` to create a unique temporary directory. Due to platform + * inconsistencies, avoid trailing `X` characters in `prefix`. Some platforms, + * notably the BSDs, can return more than six random characters, and replace + * trailing `X` characters in `prefix` with random characters. + * + * The created directory path is passed as a string to the callback's second + * parameter. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use. + * + * ```js + * import { mkdtemp } from 'node:fs'; + * import { join } from 'node:path'; + * import { tmpdir } from 'node:os'; + * + * mkdtemp(join(tmpdir(), 'foo-'), (err, directory) => { + * if (err) throw err; + * console.log(directory); + * // Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2 + * }); + * ``` + * + * The `fs.mkdtemp()` method will append the six randomly selected characters + * directly to the `prefix` string. For instance, given a directory `/tmp`, if the + * intention is to create a temporary directory _within_`/tmp`, the `prefix`must end with a trailing platform-specific path separator + * (`import { sep } from 'node:path'`). + * + * ```js + * import { tmpdir } from 'node:os'; + * import { mkdtemp } from 'node:fs'; + * + * // The parent directory for the new temporary directory + * const tmpDir = tmpdir(); + * + * // This method is *INCORRECT*: + * mkdtemp(tmpDir, (err, directory) => { + * if (err) throw err; + * console.log(directory); + * // Will print something similar to `/tmpabc123`. + * // A new temporary directory is created at the file system root + * // rather than *within* the /tmp directory. + * }); + * + * // This method is *CORRECT*: + * import { sep } from 'node:path'; + * mkdtemp(`${tmpDir}${sep}`, (err, directory) => { + * if (err) throw err; + * console.log(directory); + * // Will print something similar to `/tmp/abc123`. + * // A new temporary directory is created within + * // the /tmp directory. + * }); + * ``` + * @since v5.10.0 + */ + export function mkdtemp( + prefix: string, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, folder: string) => void, + ): void; + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function mkdtemp( + prefix: string, + options: BufferEncodingOption, + callback: (err: NodeJS.ErrnoException | null, folder: NonSharedBuffer) => void, + ): void; + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function mkdtemp( + prefix: string, + options: EncodingOption, + callback: (err: NodeJS.ErrnoException | null, folder: string | NonSharedBuffer) => void, + ): void; + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + */ + export function mkdtemp( + prefix: string, + callback: (err: NodeJS.ErrnoException | null, folder: string) => void, + ): void; + export namespace mkdtemp { + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(prefix: string, options?: EncodingOption): Promise; + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(prefix: string, options: BufferEncodingOption): Promise; + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__(prefix: string, options?: EncodingOption): Promise; + } + /** + * Returns the created directory path. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link mkdtemp}. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use. + * @since v5.10.0 + */ + export function mkdtempSync(prefix: string, options?: EncodingOption): string; + /** + * Synchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function mkdtempSync(prefix: string, options: BufferEncodingOption): NonSharedBuffer; + /** + * Synchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function mkdtempSync(prefix: string, options?: EncodingOption): string | NonSharedBuffer; + /** + * Reads the contents of a directory. The callback gets two arguments `(err, files)` where `files` is an array of the names of the files in the directory excluding `'.'` and `'..'`. + * + * See the POSIX [`readdir(3)`](http://man7.org/linux/man-pages/man3/readdir.3.html) documentation for more details. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the filenames passed to the callback. If the `encoding` is set to `'buffer'`, + * the filenames returned will be passed as `Buffer` objects. + * + * If `options.withFileTypes` is set to `true`, the `files` array will contain `fs.Dirent` objects. + * @since v0.1.8 + */ + export function readdir( + path: PathLike, + options: + | { + encoding: BufferEncoding | null; + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + } + | BufferEncoding + | undefined + | null, + callback: (err: NodeJS.ErrnoException | null, files: string[]) => void, + ): void; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readdir( + path: PathLike, + options: + | { + encoding: "buffer"; + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + } + | "buffer", + callback: (err: NodeJS.ErrnoException | null, files: NonSharedBuffer[]) => void, + ): void; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readdir( + path: PathLike, + options: + | (ObjectEncodingOptions & { + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + }) + | BufferEncoding + | undefined + | null, + callback: (err: NodeJS.ErrnoException | null, files: string[] | NonSharedBuffer[]) => void, + ): void; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export function readdir( + path: PathLike, + callback: (err: NodeJS.ErrnoException | null, files: string[]) => void, + ): void; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options If called with `withFileTypes: true` the result data will be an array of Dirent. + */ + export function readdir( + path: PathLike, + options: ObjectEncodingOptions & { + withFileTypes: true; + recursive?: boolean | undefined; + }, + callback: (err: NodeJS.ErrnoException | null, files: Dirent[]) => void, + ): void; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Must include `withFileTypes: true` and `encoding: 'buffer'`. + */ + export function readdir( + path: PathLike, + options: { + encoding: "buffer"; + withFileTypes: true; + recursive?: boolean | undefined; + }, + callback: (err: NodeJS.ErrnoException | null, files: Dirent[]) => void, + ): void; + export namespace readdir { + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__( + path: PathLike, + options?: + | { + encoding: BufferEncoding | null; + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + } + | BufferEncoding + | null, + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__( + path: PathLike, + options: + | "buffer" + | { + encoding: "buffer"; + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + }, + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function __promisify__( + path: PathLike, + options?: + | (ObjectEncodingOptions & { + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + }) + | BufferEncoding + | null, + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options If called with `withFileTypes: true` the result data will be an array of Dirent + */ + function __promisify__( + path: PathLike, + options: ObjectEncodingOptions & { + withFileTypes: true; + recursive?: boolean | undefined; + }, + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Must include `withFileTypes: true` and `encoding: 'buffer'`. + */ + function __promisify__( + path: PathLike, + options: { + encoding: "buffer"; + withFileTypes: true; + recursive?: boolean | undefined; + }, + ): Promise[]>; + } + /** + * Reads the contents of the directory. + * + * See the POSIX [`readdir(3)`](http://man7.org/linux/man-pages/man3/readdir.3.html) documentation for more details. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the filenames returned. If the `encoding` is set to `'buffer'`, + * the filenames returned will be passed as `Buffer` objects. + * + * If `options.withFileTypes` is set to `true`, the result will contain `fs.Dirent` objects. + * @since v0.1.21 + */ + export function readdirSync( + path: PathLike, + options?: + | { + encoding: BufferEncoding | null; + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + } + | BufferEncoding + | null, + ): string[]; + /** + * Synchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readdirSync( + path: PathLike, + options: + | { + encoding: "buffer"; + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + } + | "buffer", + ): NonSharedBuffer[]; + /** + * Synchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + export function readdirSync( + path: PathLike, + options?: + | (ObjectEncodingOptions & { + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + }) + | BufferEncoding + | null, + ): string[] | NonSharedBuffer[]; + /** + * Synchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options If called with `withFileTypes: true` the result data will be an array of Dirent. + */ + export function readdirSync( + path: PathLike, + options: ObjectEncodingOptions & { + withFileTypes: true; + recursive?: boolean | undefined; + }, + ): Dirent[]; + /** + * Synchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Must include `withFileTypes: true` and `encoding: 'buffer'`. + */ + export function readdirSync( + path: PathLike, + options: { + encoding: "buffer"; + withFileTypes: true; + recursive?: boolean | undefined; + }, + ): Dirent[]; + /** + * Closes the file descriptor. No arguments other than a possible exception are + * given to the completion callback. + * + * Calling `fs.close()` on any file descriptor (`fd`) that is currently in use + * through any other `fs` operation may lead to undefined behavior. + * + * See the POSIX [`close(2)`](http://man7.org/linux/man-pages/man2/close.2.html) documentation for more detail. + * @since v0.0.2 + */ + export function close(fd: number, callback?: NoParamCallback): void; + export namespace close { + /** + * Asynchronous close(2) - close a file descriptor. + * @param fd A file descriptor. + */ + function __promisify__(fd: number): Promise; + } + /** + * Closes the file descriptor. Returns `undefined`. + * + * Calling `fs.closeSync()` on any file descriptor (`fd`) that is currently in use + * through any other `fs` operation may lead to undefined behavior. + * + * See the POSIX [`close(2)`](http://man7.org/linux/man-pages/man2/close.2.html) documentation for more detail. + * @since v0.1.21 + */ + export function closeSync(fd: number): void; + /** + * Asynchronous file open. See the POSIX [`open(2)`](http://man7.org/linux/man-pages/man2/open.2.html) documentation for more details. + * + * `mode` sets the file mode (permission and sticky bits), but only if the file was + * created. On Windows, only the write permission can be manipulated; see {@link chmod}. + * + * The callback gets two arguments `(err, fd)`. + * + * Some characters (`< > : " / \ | ? *`) are reserved under Windows as documented + * by [Naming Files, Paths, and Namespaces](https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file). Under NTFS, if the filename contains + * a colon, Node.js will open a file system stream, as described by [this MSDN page](https://docs.microsoft.com/en-us/windows/desktop/FileIO/using-streams). + * + * Functions based on `fs.open()` exhibit this behavior as well:`fs.writeFile()`, `fs.readFile()`, etc. + * @since v0.0.2 + * @param [flags='r'] See `support of file system `flags``. + * @param [mode=0o666] + */ + export function open( + path: PathLike, + flags: OpenMode | undefined, + mode: Mode | undefined | null, + callback: (err: NodeJS.ErrnoException | null, fd: number) => void, + ): void; + /** + * Asynchronous open(2) - open and possibly create a file. If the file is created, its mode will be `0o666`. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param [flags='r'] See `support of file system `flags``. + */ + export function open( + path: PathLike, + flags: OpenMode | undefined, + callback: (err: NodeJS.ErrnoException | null, fd: number) => void, + ): void; + /** + * Asynchronous open(2) - open and possibly create a file. If the file is created, its mode will be `0o666`. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + */ + export function open(path: PathLike, callback: (err: NodeJS.ErrnoException | null, fd: number) => void): void; + export namespace open { + /** + * Asynchronous open(2) - open and possibly create a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param mode A file mode. If a string is passed, it is parsed as an octal integer. If not supplied, defaults to `0o666`. + */ + function __promisify__(path: PathLike, flags: OpenMode, mode?: Mode | null): Promise; + } + /** + * Returns an integer representing the file descriptor. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link open}. + * @since v0.1.21 + * @param [flags='r'] + * @param [mode=0o666] + */ + export function openSync(path: PathLike, flags: OpenMode, mode?: Mode | null): number; + /** + * Change the file system timestamps of the object referenced by `path`. + * + * The `atime` and `mtime` arguments follow these rules: + * + * * Values can be either numbers representing Unix epoch time in seconds, `Date`s, or a numeric string like `'123456789.0'`. + * * If the value can not be converted to a number, or is `NaN`, `Infinity`, or `-Infinity`, an `Error` will be thrown. + * @since v0.4.2 + */ + export function utimes(path: PathLike, atime: TimeLike, mtime: TimeLike, callback: NoParamCallback): void; + export namespace utimes { + /** + * Asynchronously change file timestamps of the file referenced by the supplied path. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param atime The last access time. If a string is provided, it will be coerced to number. + * @param mtime The last modified time. If a string is provided, it will be coerced to number. + */ + function __promisify__(path: PathLike, atime: TimeLike, mtime: TimeLike): Promise; + } + /** + * Returns `undefined`. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link utimes}. + * @since v0.4.2 + */ + export function utimesSync(path: PathLike, atime: TimeLike, mtime: TimeLike): void; + /** + * Change the file system timestamps of the object referenced by the supplied file + * descriptor. See {@link utimes}. + * @since v0.4.2 + */ + export function futimes(fd: number, atime: TimeLike, mtime: TimeLike, callback: NoParamCallback): void; + export namespace futimes { + /** + * Asynchronously change file timestamps of the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param atime The last access time. If a string is provided, it will be coerced to number. + * @param mtime The last modified time. If a string is provided, it will be coerced to number. + */ + function __promisify__(fd: number, atime: TimeLike, mtime: TimeLike): Promise; + } + /** + * Synchronous version of {@link futimes}. Returns `undefined`. + * @since v0.4.2 + */ + export function futimesSync(fd: number, atime: TimeLike, mtime: TimeLike): void; + /** + * Request that all data for the open file descriptor is flushed to the storage + * device. The specific implementation is operating system and device specific. + * Refer to the POSIX [`fsync(2)`](http://man7.org/linux/man-pages/man2/fsync.2.html) documentation for more detail. No arguments other + * than a possible exception are given to the completion callback. + * @since v0.1.96 + */ + export function fsync(fd: number, callback: NoParamCallback): void; + export namespace fsync { + /** + * Asynchronous fsync(2) - synchronize a file's in-core state with the underlying storage device. + * @param fd A file descriptor. + */ + function __promisify__(fd: number): Promise; + } + /** + * Request that all data for the open file descriptor is flushed to the storage + * device. The specific implementation is operating system and device specific. + * Refer to the POSIX [`fsync(2)`](http://man7.org/linux/man-pages/man2/fsync.2.html) documentation for more detail. Returns `undefined`. + * @since v0.1.96 + */ + export function fsyncSync(fd: number): void; + export interface WriteOptions { + /** + * @default 0 + */ + offset?: number | undefined; + /** + * @default `buffer.byteLength - offset` + */ + length?: number | undefined; + /** + * @default null + */ + position?: number | null | undefined; + } + /** + * Write `buffer` to the file specified by `fd`. + * + * `offset` determines the part of the buffer to be written, and `length` is + * an integer specifying the number of bytes to write. + * + * `position` refers to the offset from the beginning of the file where this data + * should be written. If `typeof position !== 'number'`, the data will be written + * at the current position. See [`pwrite(2)`](http://man7.org/linux/man-pages/man2/pwrite.2.html). + * + * The callback will be given three arguments `(err, bytesWritten, buffer)` where `bytesWritten` specifies how many _bytes_ were written from `buffer`. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a promise for an `Object` with `bytesWritten` and `buffer` properties. + * + * It is unsafe to use `fs.write()` multiple times on the same file without waiting + * for the callback. For this scenario, {@link createWriteStream} is + * recommended. + * + * On Linux, positional writes don't work when the file is opened in append mode. + * The kernel ignores the position argument and always appends the data to + * the end of the file. + * @since v0.0.2 + * @param [offset=0] + * @param [length=buffer.byteLength - offset] + * @param [position='null'] + */ + export function write( + fd: number, + buffer: TBuffer, + offset: number | undefined | null, + length: number | undefined | null, + position: number | undefined | null, + callback: (err: NodeJS.ErrnoException | null, written: number, buffer: TBuffer) => void, + ): void; + /** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param offset The part of the buffer to be written. If not supplied, defaults to `0`. + * @param length The number of bytes to write. If not supplied, defaults to `buffer.length - offset`. + */ + export function write( + fd: number, + buffer: TBuffer, + offset: number | undefined | null, + length: number | undefined | null, + callback: (err: NodeJS.ErrnoException | null, written: number, buffer: TBuffer) => void, + ): void; + /** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param offset The part of the buffer to be written. If not supplied, defaults to `0`. + */ + export function write( + fd: number, + buffer: TBuffer, + offset: number | undefined | null, + callback: (err: NodeJS.ErrnoException | null, written: number, buffer: TBuffer) => void, + ): void; + /** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + */ + export function write( + fd: number, + buffer: TBuffer, + callback: (err: NodeJS.ErrnoException | null, written: number, buffer: TBuffer) => void, + ): void; + /** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param options An object with the following properties: + * * `offset` The part of the buffer to be written. If not supplied, defaults to `0`. + * * `length` The number of bytes to write. If not supplied, defaults to `buffer.length - offset`. + * * `position` The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + */ + export function write( + fd: number, + buffer: TBuffer, + options: WriteOptions, + callback: (err: NodeJS.ErrnoException | null, written: number, buffer: TBuffer) => void, + ): void; + /** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + * @param encoding The expected string encoding. + */ + export function write( + fd: number, + string: string, + position: number | undefined | null, + encoding: BufferEncoding | undefined | null, + callback: (err: NodeJS.ErrnoException | null, written: number, str: string) => void, + ): void; + /** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + */ + export function write( + fd: number, + string: string, + position: number | undefined | null, + callback: (err: NodeJS.ErrnoException | null, written: number, str: string) => void, + ): void; + /** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + */ + export function write( + fd: number, + string: string, + callback: (err: NodeJS.ErrnoException | null, written: number, str: string) => void, + ): void; + export namespace write { + /** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param offset The part of the buffer to be written. If not supplied, defaults to `0`. + * @param length The number of bytes to write. If not supplied, defaults to `buffer.length - offset`. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + */ + function __promisify__( + fd: number, + buffer?: TBuffer, + offset?: number, + length?: number, + position?: number | null, + ): Promise<{ + bytesWritten: number; + buffer: TBuffer; + }>; + /** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param options An object with the following properties: + * * `offset` The part of the buffer to be written. If not supplied, defaults to `0`. + * * `length` The number of bytes to write. If not supplied, defaults to `buffer.length - offset`. + * * `position` The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + */ + function __promisify__( + fd: number, + buffer?: TBuffer, + options?: WriteOptions, + ): Promise<{ + bytesWritten: number; + buffer: TBuffer; + }>; + /** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + * @param encoding The expected string encoding. + */ + function __promisify__( + fd: number, + string: string, + position?: number | null, + encoding?: BufferEncoding | null, + ): Promise<{ + bytesWritten: number; + buffer: string; + }>; + } + /** + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link write}. + * @since v0.1.21 + * @param [offset=0] + * @param [length=buffer.byteLength - offset] + * @param [position='null'] + * @return The number of bytes written. + */ + export function writeSync( + fd: number, + buffer: NodeJS.ArrayBufferView, + offset?: number | null, + length?: number | null, + position?: number | null, + ): number; + /** + * Synchronously writes `string` to the file referenced by the supplied file descriptor, returning the number of bytes written. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + * @param encoding The expected string encoding. + */ + export function writeSync( + fd: number, + string: string, + position?: number | null, + encoding?: BufferEncoding | null, + ): number; + export type ReadPosition = number | bigint; + export interface ReadOptions { + /** + * @default 0 + */ + offset?: number | undefined; + /** + * @default `length of buffer` + */ + length?: number | undefined; + /** + * @default null + */ + position?: ReadPosition | null | undefined; + } + export interface ReadOptionsWithBuffer extends ReadOptions { + buffer?: T | undefined; + } + /** @deprecated Use `ReadOptions` instead. */ + // TODO: remove in future major + export interface ReadSyncOptions extends ReadOptions {} + /** @deprecated Use `ReadOptionsWithBuffer` instead. */ + // TODO: remove in future major + export interface ReadAsyncOptions extends ReadOptionsWithBuffer {} + /** + * Read data from the file specified by `fd`. + * + * The callback is given the three arguments, `(err, bytesRead, buffer)`. + * + * If the file is not modified concurrently, the end-of-file is reached when the + * number of bytes read is zero. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a promise for an `Object` with `bytesRead` and `buffer` properties. + * @since v0.0.2 + * @param buffer The buffer that the data will be written to. + * @param offset The position in `buffer` to write the data to. + * @param length The number of bytes to read. + * @param position Specifies where to begin reading from in the file. If `position` is `null` or `-1 `, data will be read from the current file position, and the file position will be updated. If + * `position` is an integer, the file position will be unchanged. + */ + export function read( + fd: number, + buffer: TBuffer, + offset: number, + length: number, + position: ReadPosition | null, + callback: (err: NodeJS.ErrnoException | null, bytesRead: number, buffer: TBuffer) => void, + ): void; + /** + * Similar to the above `fs.read` function, this version takes an optional `options` object. + * If not otherwise specified in an `options` object, + * `buffer` defaults to `Buffer.alloc(16384)`, + * `offset` defaults to `0`, + * `length` defaults to `buffer.byteLength`, `- offset` as of Node 17.6.0 + * `position` defaults to `null` + * @since v12.17.0, 13.11.0 + */ + export function read( + fd: number, + options: ReadOptionsWithBuffer, + callback: (err: NodeJS.ErrnoException | null, bytesRead: number, buffer: TBuffer) => void, + ): void; + export function read( + fd: number, + buffer: TBuffer, + options: ReadOptions, + callback: (err: NodeJS.ErrnoException | null, bytesRead: number, buffer: TBuffer) => void, + ): void; + export function read( + fd: number, + buffer: TBuffer, + callback: (err: NodeJS.ErrnoException | null, bytesRead: number, buffer: TBuffer) => void, + ): void; + export function read( + fd: number, + callback: (err: NodeJS.ErrnoException | null, bytesRead: number, buffer: NonSharedBuffer) => void, + ): void; + export namespace read { + /** + * @param fd A file descriptor. + * @param buffer The buffer that the data will be written to. + * @param offset The offset in the buffer at which to start writing. + * @param length The number of bytes to read. + * @param position The offset from the beginning of the file from which data should be read. If `null`, data will be read from the current position. + */ + function __promisify__( + fd: number, + buffer: TBuffer, + offset: number, + length: number, + position: ReadPosition | null, + ): Promise<{ + bytesRead: number; + buffer: TBuffer; + }>; + function __promisify__( + fd: number, + options: ReadOptionsWithBuffer, + ): Promise<{ + bytesRead: number; + buffer: TBuffer; + }>; + function __promisify__(fd: number): Promise<{ + bytesRead: number; + buffer: NonSharedBuffer; + }>; + } + /** + * Returns the number of `bytesRead`. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link read}. + * @since v0.1.21 + * @param [position='null'] + */ + export function readSync( + fd: number, + buffer: NodeJS.ArrayBufferView, + offset: number, + length: number, + position: ReadPosition | null, + ): number; + /** + * Similar to the above `fs.readSync` function, this version takes an optional `options` object. + * If no `options` object is specified, it will default with the above values. + */ + export function readSync(fd: number, buffer: NodeJS.ArrayBufferView, opts?: ReadOptions): number; + /** + * Asynchronously reads the entire contents of a file. + * + * ```js + * import { readFile } from 'node:fs'; + * + * readFile('/etc/passwd', (err, data) => { + * if (err) throw err; + * console.log(data); + * }); + * ``` + * + * The callback is passed two arguments `(err, data)`, where `data` is the + * contents of the file. + * + * If no encoding is specified, then the raw buffer is returned. + * + * If `options` is a string, then it specifies the encoding: + * + * ```js + * import { readFile } from 'node:fs'; + * + * readFile('/etc/passwd', 'utf8', callback); + * ``` + * + * When the path is a directory, the behavior of `fs.readFile()` and {@link readFileSync} is platform-specific. On macOS, Linux, and Windows, an + * error will be returned. On FreeBSD, a representation of the directory's contents + * will be returned. + * + * ```js + * import { readFile } from 'node:fs'; + * + * // macOS, Linux, and Windows + * readFile('', (err, data) => { + * // => [Error: EISDIR: illegal operation on a directory, read ] + * }); + * + * // FreeBSD + * readFile('', (err, data) => { + * // => null, + * }); + * ``` + * + * It is possible to abort an ongoing request using an `AbortSignal`. If a + * request is aborted the callback is called with an `AbortError`: + * + * ```js + * import { readFile } from 'node:fs'; + * + * const controller = new AbortController(); + * const signal = controller.signal; + * readFile(fileInfo[0].name, { signal }, (err, buf) => { + * // ... + * }); + * // When you want to abort the request + * controller.abort(); + * ``` + * + * The `fs.readFile()` function buffers the entire file. To minimize memory costs, + * when possible prefer streaming via `fs.createReadStream()`. + * + * Aborting an ongoing request does not abort individual operating + * system requests but rather the internal buffering `fs.readFile` performs. + * @since v0.1.29 + * @param path filename or file descriptor + */ + export function readFile( + path: PathOrFileDescriptor, + options: + | ({ + encoding?: null | undefined; + flag?: string | undefined; + } & Abortable) + | undefined + | null, + callback: (err: NodeJS.ErrnoException | null, data: NonSharedBuffer) => void, + ): void; + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param options Either the encoding for the result, or an object that contains the encoding and an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + export function readFile( + path: PathOrFileDescriptor, + options: + | ({ + encoding: BufferEncoding; + flag?: string | undefined; + } & Abortable) + | BufferEncoding, + callback: (err: NodeJS.ErrnoException | null, data: string) => void, + ): void; + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param options Either the encoding for the result, or an object that contains the encoding and an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + export function readFile( + path: PathOrFileDescriptor, + options: + | (ObjectEncodingOptions & { + flag?: string | undefined; + } & Abortable) + | BufferEncoding + | undefined + | null, + callback: (err: NodeJS.ErrnoException | null, data: string | NonSharedBuffer) => void, + ): void; + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + */ + export function readFile( + path: PathOrFileDescriptor, + callback: (err: NodeJS.ErrnoException | null, data: NonSharedBuffer) => void, + ): void; + export namespace readFile { + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param options An object that may contain an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + function __promisify__( + path: PathOrFileDescriptor, + options?: { + encoding?: null | undefined; + flag?: string | undefined; + } | null, + ): Promise; + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param options Either the encoding for the result, or an object that contains the encoding and an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + function __promisify__( + path: PathOrFileDescriptor, + options: + | { + encoding: BufferEncoding; + flag?: string | undefined; + } + | BufferEncoding, + ): Promise; + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param options Either the encoding for the result, or an object that contains the encoding and an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + function __promisify__( + path: PathOrFileDescriptor, + options?: + | (ObjectEncodingOptions & { + flag?: string | undefined; + }) + | BufferEncoding + | null, + ): Promise; + } + /** + * Returns the contents of the `path`. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link readFile}. + * + * If the `encoding` option is specified then this function returns a + * string. Otherwise it returns a buffer. + * + * Similar to {@link readFile}, when the path is a directory, the behavior of `fs.readFileSync()` is platform-specific. + * + * ```js + * import { readFileSync } from 'node:fs'; + * + * // macOS, Linux, and Windows + * readFileSync(''); + * // => [Error: EISDIR: illegal operation on a directory, read ] + * + * // FreeBSD + * readFileSync(''); // => + * ``` + * @since v0.1.8 + * @param path filename or file descriptor + */ + export function readFileSync( + path: PathOrFileDescriptor, + options?: { + encoding?: null | undefined; + flag?: string | undefined; + } | null, + ): NonSharedBuffer; + /** + * Synchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param options Either the encoding for the result, or an object that contains the encoding and an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + export function readFileSync( + path: PathOrFileDescriptor, + options: + | { + encoding: BufferEncoding; + flag?: string | undefined; + } + | BufferEncoding, + ): string; + /** + * Synchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param options Either the encoding for the result, or an object that contains the encoding and an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + export function readFileSync( + path: PathOrFileDescriptor, + options?: + | (ObjectEncodingOptions & { + flag?: string | undefined; + }) + | BufferEncoding + | null, + ): string | NonSharedBuffer; + export type WriteFileOptions = + | ( + & ObjectEncodingOptions + & Abortable + & { + mode?: Mode | undefined; + flag?: string | undefined; + flush?: boolean | undefined; + } + ) + | BufferEncoding + | null; + /** + * When `file` is a filename, asynchronously writes data to the file, replacing the + * file if it already exists. `data` can be a string or a buffer. + * + * When `file` is a file descriptor, the behavior is similar to calling `fs.write()` directly (which is recommended). See the notes below on using + * a file descriptor. + * + * The `encoding` option is ignored if `data` is a buffer. + * + * The `mode` option only affects the newly created file. See {@link open} for more details. + * + * ```js + * import { writeFile } from 'node:fs'; + * import { Buffer } from 'node:buffer'; + * + * const data = new Uint8Array(Buffer.from('Hello Node.js')); + * writeFile('message.txt', data, (err) => { + * if (err) throw err; + * console.log('The file has been saved!'); + * }); + * ``` + * + * If `options` is a string, then it specifies the encoding: + * + * ```js + * import { writeFile } from 'node:fs'; + * + * writeFile('message.txt', 'Hello Node.js', 'utf8', callback); + * ``` + * + * It is unsafe to use `fs.writeFile()` multiple times on the same file without + * waiting for the callback. For this scenario, {@link createWriteStream} is + * recommended. + * + * Similarly to `fs.readFile` \- `fs.writeFile` is a convenience method that + * performs multiple `write` calls internally to write the buffer passed to it. + * For performance sensitive code consider using {@link createWriteStream}. + * + * It is possible to use an `AbortSignal` to cancel an `fs.writeFile()`. + * Cancelation is "best effort", and some amount of data is likely still + * to be written. + * + * ```js + * import { writeFile } from 'node:fs'; + * import { Buffer } from 'node:buffer'; + * + * const controller = new AbortController(); + * const { signal } = controller; + * const data = new Uint8Array(Buffer.from('Hello Node.js')); + * writeFile('message.txt', data, { signal }, (err) => { + * // When a request is aborted - the callback is called with an AbortError + * }); + * // When the request should be aborted + * controller.abort(); + * ``` + * + * Aborting an ongoing request does not abort individual operating + * system requests but rather the internal buffering `fs.writeFile` performs. + * @since v0.1.29 + * @param file filename or file descriptor + */ + export function writeFile( + file: PathOrFileDescriptor, + data: string | NodeJS.ArrayBufferView, + options: WriteFileOptions, + callback: NoParamCallback, + ): void; + /** + * Asynchronously writes data to a file, replacing the file if it already exists. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param data The data to write. If something other than a Buffer or Uint8Array is provided, the value is coerced to a string. + */ + export function writeFile( + path: PathOrFileDescriptor, + data: string | NodeJS.ArrayBufferView, + callback: NoParamCallback, + ): void; + export namespace writeFile { + /** + * Asynchronously writes data to a file, replacing the file if it already exists. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param data The data to write. If something other than a Buffer or Uint8Array is provided, the value is coerced to a string. + * @param options Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `mode` is not supplied, the default of `0o666` is used. + * If `mode` is a string, it is parsed as an octal integer. + * If `flag` is not supplied, the default of `'w'` is used. + */ + function __promisify__( + path: PathOrFileDescriptor, + data: string | NodeJS.ArrayBufferView, + options?: WriteFileOptions, + ): Promise; + } + /** + * Returns `undefined`. + * + * The `mode` option only affects the newly created file. See {@link open} for more details. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link writeFile}. + * @since v0.1.29 + * @param file filename or file descriptor + */ + export function writeFileSync( + file: PathOrFileDescriptor, + data: string | NodeJS.ArrayBufferView, + options?: WriteFileOptions, + ): void; + /** + * Asynchronously append data to a file, creating the file if it does not yet + * exist. `data` can be a string or a `Buffer`. + * + * The `mode` option only affects the newly created file. See {@link open} for more details. + * + * ```js + * import { appendFile } from 'node:fs'; + * + * appendFile('message.txt', 'data to append', (err) => { + * if (err) throw err; + * console.log('The "data to append" was appended to file!'); + * }); + * ``` + * + * If `options` is a string, then it specifies the encoding: + * + * ```js + * import { appendFile } from 'node:fs'; + * + * appendFile('message.txt', 'data to append', 'utf8', callback); + * ``` + * + * The `path` may be specified as a numeric file descriptor that has been opened + * for appending (using `fs.open()` or `fs.openSync()`). The file descriptor will + * not be closed automatically. + * + * ```js + * import { open, close, appendFile } from 'node:fs'; + * + * function closeFd(fd) { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * + * open('message.txt', 'a', (err, fd) => { + * if (err) throw err; + * + * try { + * appendFile(fd, 'data to append', 'utf8', (err) => { + * closeFd(fd); + * if (err) throw err; + * }); + * } catch (err) { + * closeFd(fd); + * throw err; + * } + * }); + * ``` + * @since v0.6.7 + * @param path filename or file descriptor + */ + export function appendFile( + path: PathOrFileDescriptor, + data: string | Uint8Array, + options: WriteFileOptions, + callback: NoParamCallback, + ): void; + /** + * Asynchronously append data to a file, creating the file if it does not exist. + * @param file A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param data The data to write. If something other than a Buffer or Uint8Array is provided, the value is coerced to a string. + */ + export function appendFile(file: PathOrFileDescriptor, data: string | Uint8Array, callback: NoParamCallback): void; + export namespace appendFile { + /** + * Asynchronously append data to a file, creating the file if it does not exist. + * @param file A path to a file. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + * If a file descriptor is provided, the underlying file will _not_ be closed automatically. + * @param data The data to write. If something other than a Buffer or Uint8Array is provided, the value is coerced to a string. + * @param options Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `mode` is not supplied, the default of `0o666` is used. + * If `mode` is a string, it is parsed as an octal integer. + * If `flag` is not supplied, the default of `'a'` is used. + */ + function __promisify__( + file: PathOrFileDescriptor, + data: string | Uint8Array, + options?: WriteFileOptions, + ): Promise; + } + /** + * Synchronously append data to a file, creating the file if it does not yet + * exist. `data` can be a string or a `Buffer`. + * + * The `mode` option only affects the newly created file. See {@link open} for more details. + * + * ```js + * import { appendFileSync } from 'node:fs'; + * + * try { + * appendFileSync('message.txt', 'data to append'); + * console.log('The "data to append" was appended to file!'); + * } catch (err) { + * // Handle the error + * } + * ``` + * + * If `options` is a string, then it specifies the encoding: + * + * ```js + * import { appendFileSync } from 'node:fs'; + * + * appendFileSync('message.txt', 'data to append', 'utf8'); + * ``` + * + * The `path` may be specified as a numeric file descriptor that has been opened + * for appending (using `fs.open()` or `fs.openSync()`). The file descriptor will + * not be closed automatically. + * + * ```js + * import { openSync, closeSync, appendFileSync } from 'node:fs'; + * + * let fd; + * + * try { + * fd = openSync('message.txt', 'a'); + * appendFileSync(fd, 'data to append', 'utf8'); + * } catch (err) { + * // Handle the error + * } finally { + * if (fd !== undefined) + * closeSync(fd); + * } + * ``` + * @since v0.6.7 + * @param path filename or file descriptor + */ + export function appendFileSync( + path: PathOrFileDescriptor, + data: string | Uint8Array, + options?: WriteFileOptions, + ): void; + /** + * Watch for changes on `filename`. The callback `listener` will be called each + * time the file is accessed. + * + * The `options` argument may be omitted. If provided, it should be an object. The `options` object may contain a boolean named `persistent` that indicates + * whether the process should continue to run as long as files are being watched. + * The `options` object may specify an `interval` property indicating how often the + * target should be polled in milliseconds. + * + * The `listener` gets two arguments the current stat object and the previous + * stat object: + * + * ```js + * import { watchFile } from 'node:fs'; + * + * watchFile('message.text', (curr, prev) => { + * console.log(`the current mtime is: ${curr.mtime}`); + * console.log(`the previous mtime was: ${prev.mtime}`); + * }); + * ``` + * + * These stat objects are instances of `fs.Stat`. If the `bigint` option is `true`, + * the numeric values in these objects are specified as `BigInt`s. + * + * To be notified when the file was modified, not just accessed, it is necessary + * to compare `curr.mtimeMs` and `prev.mtimeMs`. + * + * When an `fs.watchFile` operation results in an `ENOENT` error, it + * will invoke the listener once, with all the fields zeroed (or, for dates, the + * Unix Epoch). If the file is created later on, the listener will be called + * again, with the latest stat objects. This is a change in functionality since + * v0.10. + * + * Using {@link watch} is more efficient than `fs.watchFile` and `fs.unwatchFile`. `fs.watch` should be used instead of `fs.watchFile` and `fs.unwatchFile` when possible. + * + * When a file being watched by `fs.watchFile()` disappears and reappears, + * then the contents of `previous` in the second callback event (the file's + * reappearance) will be the same as the contents of `previous` in the first + * callback event (its disappearance). + * + * This happens when: + * + * * the file is deleted, followed by a restore + * * the file is renamed and then renamed a second time back to its original name + * @since v0.1.31 + */ + export interface WatchFileOptions { + bigint?: boolean | undefined; + persistent?: boolean | undefined; + interval?: number | undefined; + } + /** + * Watch for changes on `filename`. The callback `listener` will be called each + * time the file is accessed. + * + * The `options` argument may be omitted. If provided, it should be an object. The `options` object may contain a boolean named `persistent` that indicates + * whether the process should continue to run as long as files are being watched. + * The `options` object may specify an `interval` property indicating how often the + * target should be polled in milliseconds. + * + * The `listener` gets two arguments the current stat object and the previous + * stat object: + * + * ```js + * import { watchFile } from 'node:fs'; + * + * watchFile('message.text', (curr, prev) => { + * console.log(`the current mtime is: ${curr.mtime}`); + * console.log(`the previous mtime was: ${prev.mtime}`); + * }); + * ``` + * + * These stat objects are instances of `fs.Stat`. If the `bigint` option is `true`, + * the numeric values in these objects are specified as `BigInt`s. + * + * To be notified when the file was modified, not just accessed, it is necessary + * to compare `curr.mtimeMs` and `prev.mtimeMs`. + * + * When an `fs.watchFile` operation results in an `ENOENT` error, it + * will invoke the listener once, with all the fields zeroed (or, for dates, the + * Unix Epoch). If the file is created later on, the listener will be called + * again, with the latest stat objects. This is a change in functionality since + * v0.10. + * + * Using {@link watch} is more efficient than `fs.watchFile` and `fs.unwatchFile`. `fs.watch` should be used instead of `fs.watchFile` and `fs.unwatchFile` when possible. + * + * When a file being watched by `fs.watchFile()` disappears and reappears, + * then the contents of `previous` in the second callback event (the file's + * reappearance) will be the same as the contents of `previous` in the first + * callback event (its disappearance). + * + * This happens when: + * + * * the file is deleted, followed by a restore + * * the file is renamed and then renamed a second time back to its original name + * @since v0.1.31 + */ + export function watchFile( + filename: PathLike, + options: + | (WatchFileOptions & { + bigint?: false | undefined; + }) + | undefined, + listener: StatsListener, + ): StatWatcher; + export function watchFile( + filename: PathLike, + options: + | (WatchFileOptions & { + bigint: true; + }) + | undefined, + listener: BigIntStatsListener, + ): StatWatcher; + /** + * Watch for changes on `filename`. The callback `listener` will be called each time the file is accessed. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + export function watchFile(filename: PathLike, listener: StatsListener): StatWatcher; + /** + * Stop watching for changes on `filename`. If `listener` is specified, only that + * particular listener is removed. Otherwise, _all_ listeners are removed, + * effectively stopping watching of `filename`. + * + * Calling `fs.unwatchFile()` with a filename that is not being watched is a + * no-op, not an error. + * + * Using {@link watch} is more efficient than `fs.watchFile()` and `fs.unwatchFile()`. `fs.watch()` should be used instead of `fs.watchFile()` and `fs.unwatchFile()` when possible. + * @since v0.1.31 + * @param listener Optional, a listener previously attached using `fs.watchFile()` + */ + export function unwatchFile(filename: PathLike, listener?: StatsListener): void; + export function unwatchFile(filename: PathLike, listener?: BigIntStatsListener): void; + export interface WatchOptions extends Abortable { + encoding?: BufferEncoding | "buffer" | undefined; + persistent?: boolean | undefined; + recursive?: boolean | undefined; + } + export interface WatchOptionsWithBufferEncoding extends WatchOptions { + encoding: "buffer"; + } + export interface WatchOptionsWithStringEncoding extends WatchOptions { + encoding?: BufferEncoding | undefined; + } + export type WatchEventType = "rename" | "change"; + export type WatchListener = (event: WatchEventType, filename: T | null) => void; + export type StatsListener = (curr: Stats, prev: Stats) => void; + export type BigIntStatsListener = (curr: BigIntStats, prev: BigIntStats) => void; + /** + * Watch for changes on `filename`, where `filename` is either a file or a + * directory. + * + * The second argument is optional. If `options` is provided as a string, it + * specifies the `encoding`. Otherwise `options` should be passed as an object. + * + * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'` or `'change'`, and `filename` is the name of the file + * which triggered the event. + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * The listener callback is attached to the `'change'` event fired by `fs.FSWatcher`, but it is not the same thing as the `'change'` value of `eventType`. + * + * If a `signal` is passed, aborting the corresponding AbortController will close + * the returned `fs.FSWatcher`. + * @since v0.5.10 + * @param listener + */ + export function watch( + filename: PathLike, + options?: WatchOptionsWithStringEncoding | BufferEncoding | null, + listener?: WatchListener, + ): FSWatcher; + export function watch( + filename: PathLike, + options: WatchOptionsWithBufferEncoding | "buffer", + listener: WatchListener, + ): FSWatcher; + export function watch( + filename: PathLike, + options: WatchOptions | BufferEncoding | "buffer" | null, + listener: WatchListener, + ): FSWatcher; + export function watch(filename: PathLike, listener: WatchListener): FSWatcher; + /** + * Test whether or not the given path exists by checking with the file system. + * Then call the `callback` argument with either true or false: + * + * ```js + * import { exists } from 'node:fs'; + * + * exists('/etc/passwd', (e) => { + * console.log(e ? 'it exists' : 'no passwd!'); + * }); + * ``` + * + * **The parameters for this callback are not consistent with other Node.js** + * **callbacks.** Normally, the first parameter to a Node.js callback is an `err` parameter, optionally followed by other parameters. The `fs.exists()` callback + * has only one boolean parameter. This is one reason `fs.access()` is recommended + * instead of `fs.exists()`. + * + * Using `fs.exists()` to check for the existence of a file before calling `fs.open()`, `fs.readFile()`, or `fs.writeFile()` is not recommended. Doing + * so introduces a race condition, since other processes may change the file's + * state between the two calls. Instead, user code should open/read/write the + * file directly and handle the error raised if the file does not exist. + * + * **write (NOT RECOMMENDED)** + * + * ```js + * import { exists, open, close } from 'node:fs'; + * + * exists('myfile', (e) => { + * if (e) { + * console.error('myfile already exists'); + * } else { + * open('myfile', 'wx', (err, fd) => { + * if (err) throw err; + * + * try { + * writeMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * } + * }); + * ``` + * + * **write (RECOMMENDED)** + * + * ```js + * import { open, close } from 'node:fs'; + * open('myfile', 'wx', (err, fd) => { + * if (err) { + * if (err.code === 'EEXIST') { + * console.error('myfile already exists'); + * return; + * } + * + * throw err; + * } + * + * try { + * writeMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * ``` + * + * **read (NOT RECOMMENDED)** + * + * ```js + * import { open, close, exists } from 'node:fs'; + * + * exists('myfile', (e) => { + * if (e) { + * open('myfile', 'r', (err, fd) => { + * if (err) throw err; + * + * try { + * readMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * } else { + * console.error('myfile does not exist'); + * } + * }); + * ``` + * + * **read (RECOMMENDED)** + * + * ```js + * import { open, close } from 'node:fs'; + * + * open('myfile', 'r', (err, fd) => { + * if (err) { + * if (err.code === 'ENOENT') { + * console.error('myfile does not exist'); + * return; + * } + * + * throw err; + * } + * + * try { + * readMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * ``` + * + * The "not recommended" examples above check for existence and then use the + * file; the "recommended" examples are better because they use the file directly + * and handle the error, if any. + * + * In general, check for the existence of a file only if the file won't be + * used directly, for example when its existence is a signal from another + * process. + * @since v0.0.2 + * @deprecated Since v1.0.0 - Use {@link stat} or {@link access} instead. + */ + export function exists(path: PathLike, callback: (exists: boolean) => void): void; + /** @deprecated */ + export namespace exists { + /** + * @param path A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + */ + function __promisify__(path: PathLike): Promise; + } + /** + * Returns `true` if the path exists, `false` otherwise. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link exists}. + * + * `fs.exists()` is deprecated, but `fs.existsSync()` is not. The `callback` parameter to `fs.exists()` accepts parameters that are inconsistent with other + * Node.js callbacks. `fs.existsSync()` does not use a callback. + * + * ```js + * import { existsSync } from 'node:fs'; + * + * if (existsSync('/etc/passwd')) + * console.log('The path exists.'); + * ``` + * @since v0.1.21 + */ + export function existsSync(path: PathLike): boolean; + export namespace constants { + // File Access Constants + /** Constant for fs.access(). File is visible to the calling process. */ + const F_OK: number; + /** Constant for fs.access(). File can be read by the calling process. */ + const R_OK: number; + /** Constant for fs.access(). File can be written by the calling process. */ + const W_OK: number; + /** Constant for fs.access(). File can be executed by the calling process. */ + const X_OK: number; + // File Copy Constants + /** Constant for fs.copyFile. Flag indicating the destination file should not be overwritten if it already exists. */ + const COPYFILE_EXCL: number; + /** + * Constant for fs.copyFile. copy operation will attempt to create a copy-on-write reflink. + * If the underlying platform does not support copy-on-write, then a fallback copy mechanism is used. + */ + const COPYFILE_FICLONE: number; + /** + * Constant for fs.copyFile. Copy operation will attempt to create a copy-on-write reflink. + * If the underlying platform does not support copy-on-write, then the operation will fail with an error. + */ + const COPYFILE_FICLONE_FORCE: number; + // File Open Constants + /** Constant for fs.open(). Flag indicating to open a file for read-only access. */ + const O_RDONLY: number; + /** Constant for fs.open(). Flag indicating to open a file for write-only access. */ + const O_WRONLY: number; + /** Constant for fs.open(). Flag indicating to open a file for read-write access. */ + const O_RDWR: number; + /** Constant for fs.open(). Flag indicating to create the file if it does not already exist. */ + const O_CREAT: number; + /** Constant for fs.open(). Flag indicating that opening a file should fail if the O_CREAT flag is set and the file already exists. */ + const O_EXCL: number; + /** + * Constant for fs.open(). Flag indicating that if path identifies a terminal device, + * opening the path shall not cause that terminal to become the controlling terminal for the process + * (if the process does not already have one). + */ + const O_NOCTTY: number; + /** Constant for fs.open(). Flag indicating that if the file exists and is a regular file, and the file is opened successfully for write access, its length shall be truncated to zero. */ + const O_TRUNC: number; + /** Constant for fs.open(). Flag indicating that data will be appended to the end of the file. */ + const O_APPEND: number; + /** Constant for fs.open(). Flag indicating that the open should fail if the path is not a directory. */ + const O_DIRECTORY: number; + /** + * constant for fs.open(). + * Flag indicating reading accesses to the file system will no longer result in + * an update to the atime information associated with the file. + * This flag is available on Linux operating systems only. + */ + const O_NOATIME: number; + /** Constant for fs.open(). Flag indicating that the open should fail if the path is a symbolic link. */ + const O_NOFOLLOW: number; + /** Constant for fs.open(). Flag indicating that the file is opened for synchronous I/O. */ + const O_SYNC: number; + /** Constant for fs.open(). Flag indicating that the file is opened for synchronous I/O with write operations waiting for data integrity. */ + const O_DSYNC: number; + /** Constant for fs.open(). Flag indicating to open the symbolic link itself rather than the resource it is pointing to. */ + const O_SYMLINK: number; + /** Constant for fs.open(). When set, an attempt will be made to minimize caching effects of file I/O. */ + const O_DIRECT: number; + /** Constant for fs.open(). Flag indicating to open the file in nonblocking mode when possible. */ + const O_NONBLOCK: number; + // File Type Constants + /** Constant for fs.Stats mode property for determining a file's type. Bit mask used to extract the file type code. */ + const S_IFMT: number; + /** Constant for fs.Stats mode property for determining a file's type. File type constant for a regular file. */ + const S_IFREG: number; + /** Constant for fs.Stats mode property for determining a file's type. File type constant for a directory. */ + const S_IFDIR: number; + /** Constant for fs.Stats mode property for determining a file's type. File type constant for a character-oriented device file. */ + const S_IFCHR: number; + /** Constant for fs.Stats mode property for determining a file's type. File type constant for a block-oriented device file. */ + const S_IFBLK: number; + /** Constant for fs.Stats mode property for determining a file's type. File type constant for a FIFO/pipe. */ + const S_IFIFO: number; + /** Constant for fs.Stats mode property for determining a file's type. File type constant for a symbolic link. */ + const S_IFLNK: number; + /** Constant for fs.Stats mode property for determining a file's type. File type constant for a socket. */ + const S_IFSOCK: number; + // File Mode Constants + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable, writable and executable by owner. */ + const S_IRWXU: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable by owner. */ + const S_IRUSR: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating writable by owner. */ + const S_IWUSR: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating executable by owner. */ + const S_IXUSR: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable, writable and executable by group. */ + const S_IRWXG: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable by group. */ + const S_IRGRP: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating writable by group. */ + const S_IWGRP: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating executable by group. */ + const S_IXGRP: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable, writable and executable by others. */ + const S_IRWXO: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable by others. */ + const S_IROTH: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating writable by others. */ + const S_IWOTH: number; + /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating executable by others. */ + const S_IXOTH: number; + /** + * When set, a memory file mapping is used to access the file. This flag + * is available on Windows operating systems only. On other operating systems, + * this flag is ignored. + */ + const UV_FS_O_FILEMAP: number; + } + /** + * Tests a user's permissions for the file or directory specified by `path`. + * The `mode` argument is an optional integer that specifies the accessibility + * checks to be performed. `mode` should be either the value `fs.constants.F_OK` or a mask consisting of the bitwise OR of any of `fs.constants.R_OK`, `fs.constants.W_OK`, and `fs.constants.X_OK` + * (e.g.`fs.constants.W_OK | fs.constants.R_OK`). Check `File access constants` for + * possible values of `mode`. + * + * The final argument, `callback`, is a callback function that is invoked with + * a possible error argument. If any of the accessibility checks fail, the error + * argument will be an `Error` object. The following examples check if `package.json` exists, and if it is readable or writable. + * + * ```js + * import { access, constants } from 'node:fs'; + * + * const file = 'package.json'; + * + * // Check if the file exists in the current directory. + * access(file, constants.F_OK, (err) => { + * console.log(`${file} ${err ? 'does not exist' : 'exists'}`); + * }); + * + * // Check if the file is readable. + * access(file, constants.R_OK, (err) => { + * console.log(`${file} ${err ? 'is not readable' : 'is readable'}`); + * }); + * + * // Check if the file is writable. + * access(file, constants.W_OK, (err) => { + * console.log(`${file} ${err ? 'is not writable' : 'is writable'}`); + * }); + * + * // Check if the file is readable and writable. + * access(file, constants.R_OK | constants.W_OK, (err) => { + * console.log(`${file} ${err ? 'is not' : 'is'} readable and writable`); + * }); + * ``` + * + * Do not use `fs.access()` to check for the accessibility of a file before calling `fs.open()`, `fs.readFile()`, or `fs.writeFile()`. Doing + * so introduces a race condition, since other processes may change the file's + * state between the two calls. Instead, user code should open/read/write the + * file directly and handle the error raised if the file is not accessible. + * + * **write (NOT RECOMMENDED)** + * + * ```js + * import { access, open, close } from 'node:fs'; + * + * access('myfile', (err) => { + * if (!err) { + * console.error('myfile already exists'); + * return; + * } + * + * open('myfile', 'wx', (err, fd) => { + * if (err) throw err; + * + * try { + * writeMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * }); + * ``` + * + * **write (RECOMMENDED)** + * + * ```js + * import { open, close } from 'node:fs'; + * + * open('myfile', 'wx', (err, fd) => { + * if (err) { + * if (err.code === 'EEXIST') { + * console.error('myfile already exists'); + * return; + * } + * + * throw err; + * } + * + * try { + * writeMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * ``` + * + * **read (NOT RECOMMENDED)** + * + * ```js + * import { access, open, close } from 'node:fs'; + * access('myfile', (err) => { + * if (err) { + * if (err.code === 'ENOENT') { + * console.error('myfile does not exist'); + * return; + * } + * + * throw err; + * } + * + * open('myfile', 'r', (err, fd) => { + * if (err) throw err; + * + * try { + * readMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * }); + * ``` + * + * **read (RECOMMENDED)** + * + * ```js + * import { open, close } from 'node:fs'; + * + * open('myfile', 'r', (err, fd) => { + * if (err) { + * if (err.code === 'ENOENT') { + * console.error('myfile does not exist'); + * return; + * } + * + * throw err; + * } + * + * try { + * readMyData(fd); + * } finally { + * close(fd, (err) => { + * if (err) throw err; + * }); + * } + * }); + * ``` + * + * The "not recommended" examples above check for accessibility and then use the + * file; the "recommended" examples are better because they use the file directly + * and handle the error, if any. + * + * In general, check for the accessibility of a file only if the file will not be + * used directly, for example when its accessibility is a signal from another + * process. + * + * On Windows, access-control policies (ACLs) on a directory may limit access to + * a file or directory. The `fs.access()` function, however, does not check the + * ACL and therefore may report that a path is accessible even if the ACL restricts + * the user from reading or writing to it. + * @since v0.11.15 + * @param [mode=fs.constants.F_OK] + */ + export function access(path: PathLike, mode: number | undefined, callback: NoParamCallback): void; + /** + * Asynchronously tests a user's permissions for the file specified by path. + * @param path A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + export function access(path: PathLike, callback: NoParamCallback): void; + export namespace access { + /** + * Asynchronously tests a user's permissions for the file specified by path. + * @param path A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * URL support is _experimental_. + */ + function __promisify__(path: PathLike, mode?: number): Promise; + } + /** + * Synchronously tests a user's permissions for the file or directory specified + * by `path`. The `mode` argument is an optional integer that specifies the + * accessibility checks to be performed. `mode` should be either the value `fs.constants.F_OK` or a mask consisting of the bitwise OR of any of `fs.constants.R_OK`, `fs.constants.W_OK`, and + * `fs.constants.X_OK` (e.g.`fs.constants.W_OK | fs.constants.R_OK`). Check `File access constants` for + * possible values of `mode`. + * + * If any of the accessibility checks fail, an `Error` will be thrown. Otherwise, + * the method will return `undefined`. + * + * ```js + * import { accessSync, constants } from 'node:fs'; + * + * try { + * accessSync('etc/passwd', constants.R_OK | constants.W_OK); + * console.log('can read/write'); + * } catch (err) { + * console.error('no access!'); + * } + * ``` + * @since v0.11.15 + * @param [mode=fs.constants.F_OK] + */ + export function accessSync(path: PathLike, mode?: number): void; + interface StreamOptions { + flags?: string | undefined; + encoding?: BufferEncoding | undefined; + fd?: number | promises.FileHandle | undefined; + mode?: number | undefined; + autoClose?: boolean | undefined; + emitClose?: boolean | undefined; + start?: number | undefined; + signal?: AbortSignal | null | undefined; + highWaterMark?: number | undefined; + } + interface FSImplementation { + open?: (...args: any[]) => any; + close?: (...args: any[]) => any; + } + interface CreateReadStreamFSImplementation extends FSImplementation { + read: (...args: any[]) => any; + } + interface CreateWriteStreamFSImplementation extends FSImplementation { + write: (...args: any[]) => any; + writev?: (...args: any[]) => any; + } + interface ReadStreamOptions extends StreamOptions { + fs?: CreateReadStreamFSImplementation | null | undefined; + end?: number | undefined; + } + interface WriteStreamOptions extends StreamOptions { + fs?: CreateWriteStreamFSImplementation | null | undefined; + flush?: boolean | undefined; + } + /** + * `options` can include `start` and `end` values to read a range of bytes from + * the file instead of the entire file. Both `start` and `end` are inclusive and + * start counting at 0, allowed values are in the + * \[0, [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)\] range. If `fd` is specified and `start` is + * omitted or `undefined`, `fs.createReadStream()` reads sequentially from the + * current file position. The `encoding` can be any one of those accepted by `Buffer`. + * + * If `fd` is specified, `ReadStream` will ignore the `path` argument and will use + * the specified file descriptor. This means that no `'open'` event will be + * emitted. `fd` should be blocking; non-blocking `fd`s should be passed to `net.Socket`. + * + * If `fd` points to a character device that only supports blocking reads + * (such as keyboard or sound card), read operations do not finish until data is + * available. This can prevent the process from exiting and the stream from + * closing naturally. + * + * By default, the stream will emit a `'close'` event after it has been + * destroyed. Set the `emitClose` option to `false` to change this behavior. + * + * By providing the `fs` option, it is possible to override the corresponding `fs` implementations for `open`, `read`, and `close`. When providing the `fs` option, + * an override for `read` is required. If no `fd` is provided, an override for `open` is also required. If `autoClose` is `true`, an override for `close` is + * also required. + * + * ```js + * import { createReadStream } from 'node:fs'; + * + * // Create a stream from some character device. + * const stream = createReadStream('/dev/input/event0'); + * setTimeout(() => { + * stream.close(); // This may not close the stream. + * // Artificially marking end-of-stream, as if the underlying resource had + * // indicated end-of-file by itself, allows the stream to close. + * // This does not cancel pending read operations, and if there is such an + * // operation, the process may still not be able to exit successfully + * // until it finishes. + * stream.push(null); + * stream.read(0); + * }, 100); + * ``` + * + * If `autoClose` is false, then the file descriptor won't be closed, even if + * there's an error. It is the application's responsibility to close it and make + * sure there's no file descriptor leak. If `autoClose` is set to true (default + * behavior), on `'error'` or `'end'` the file descriptor will be closed + * automatically. + * + * `mode` sets the file mode (permission and sticky bits), but only if the + * file was created. + * + * An example to read the last 10 bytes of a file which is 100 bytes long: + * + * ```js + * import { createReadStream } from 'node:fs'; + * + * createReadStream('sample.txt', { start: 90, end: 99 }); + * ``` + * + * If `options` is a string, then it specifies the encoding. + * @since v0.1.31 + */ + export function createReadStream(path: PathLike, options?: BufferEncoding | ReadStreamOptions): ReadStream; + /** + * `options` may also include a `start` option to allow writing data at some + * position past the beginning of the file, allowed values are in the + * \[0, [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)\] range. Modifying a file rather than + * replacing it may require the `flags` option to be set to `r+` rather than the + * default `w`. The `encoding` can be any one of those accepted by `Buffer`. + * + * If `autoClose` is set to true (default behavior) on `'error'` or `'finish'` the file descriptor will be closed automatically. If `autoClose` is false, + * then the file descriptor won't be closed, even if there's an error. + * It is the application's responsibility to close it and make sure there's no + * file descriptor leak. + * + * By default, the stream will emit a `'close'` event after it has been + * destroyed. Set the `emitClose` option to `false` to change this behavior. + * + * By providing the `fs` option it is possible to override the corresponding `fs` implementations for `open`, `write`, `writev`, and `close`. Overriding `write()` without `writev()` can reduce + * performance as some optimizations (`_writev()`) + * will be disabled. When providing the `fs` option, overrides for at least one of `write` and `writev` are required. If no `fd` option is supplied, an override + * for `open` is also required. If `autoClose` is `true`, an override for `close` is also required. + * + * Like `fs.ReadStream`, if `fd` is specified, `fs.WriteStream` will ignore the `path` argument and will use the specified file descriptor. This means that no `'open'` event will be + * emitted. `fd` should be blocking; non-blocking `fd`s + * should be passed to `net.Socket`. + * + * If `options` is a string, then it specifies the encoding. + * @since v0.1.31 + */ + export function createWriteStream(path: PathLike, options?: BufferEncoding | WriteStreamOptions): WriteStream; + /** + * Forces all currently queued I/O operations associated with the file to the + * operating system's synchronized I/O completion state. Refer to the POSIX [`fdatasync(2)`](http://man7.org/linux/man-pages/man2/fdatasync.2.html) documentation for details. No arguments other + * than a possible + * exception are given to the completion callback. + * @since v0.1.96 + */ + export function fdatasync(fd: number, callback: NoParamCallback): void; + export namespace fdatasync { + /** + * Asynchronous fdatasync(2) - synchronize a file's in-core state with storage device. + * @param fd A file descriptor. + */ + function __promisify__(fd: number): Promise; + } + /** + * Forces all currently queued I/O operations associated with the file to the + * operating system's synchronized I/O completion state. Refer to the POSIX [`fdatasync(2)`](http://man7.org/linux/man-pages/man2/fdatasync.2.html) documentation for details. Returns `undefined`. + * @since v0.1.96 + */ + export function fdatasyncSync(fd: number): void; + /** + * Asynchronously copies `src` to `dest`. By default, `dest` is overwritten if it + * already exists. No arguments other than a possible exception are given to the + * callback function. Node.js makes no guarantees about the atomicity of the copy + * operation. If an error occurs after the destination file has been opened for + * writing, Node.js will attempt to remove the destination. + * + * `mode` is an optional integer that specifies the behavior + * of the copy operation. It is possible to create a mask consisting of the bitwise + * OR of two or more values (e.g.`fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE`). + * + * * `fs.constants.COPYFILE_EXCL`: The copy operation will fail if `dest` already + * exists. + * * `fs.constants.COPYFILE_FICLONE`: The copy operation will attempt to create a + * copy-on-write reflink. If the platform does not support copy-on-write, then a + * fallback copy mechanism is used. + * * `fs.constants.COPYFILE_FICLONE_FORCE`: The copy operation will attempt to + * create a copy-on-write reflink. If the platform does not support + * copy-on-write, then the operation will fail. + * + * ```js + * import { copyFile, constants } from 'node:fs'; + * + * function callback(err) { + * if (err) throw err; + * console.log('source.txt was copied to destination.txt'); + * } + * + * // destination.txt will be created or overwritten by default. + * copyFile('source.txt', 'destination.txt', callback); + * + * // By using COPYFILE_EXCL, the operation will fail if destination.txt exists. + * copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback); + * ``` + * @since v8.5.0 + * @param src source filename to copy + * @param dest destination filename of the copy operation + * @param [mode=0] modifiers for copy operation. + */ + export function copyFile(src: PathLike, dest: PathLike, callback: NoParamCallback): void; + export function copyFile(src: PathLike, dest: PathLike, mode: number, callback: NoParamCallback): void; + export namespace copyFile { + function __promisify__(src: PathLike, dst: PathLike, mode?: number): Promise; + } + /** + * Synchronously copies `src` to `dest`. By default, `dest` is overwritten if it + * already exists. Returns `undefined`. Node.js makes no guarantees about the + * atomicity of the copy operation. If an error occurs after the destination file + * has been opened for writing, Node.js will attempt to remove the destination. + * + * `mode` is an optional integer that specifies the behavior + * of the copy operation. It is possible to create a mask consisting of the bitwise + * OR of two or more values (e.g.`fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE`). + * + * * `fs.constants.COPYFILE_EXCL`: The copy operation will fail if `dest` already + * exists. + * * `fs.constants.COPYFILE_FICLONE`: The copy operation will attempt to create a + * copy-on-write reflink. If the platform does not support copy-on-write, then a + * fallback copy mechanism is used. + * * `fs.constants.COPYFILE_FICLONE_FORCE`: The copy operation will attempt to + * create a copy-on-write reflink. If the platform does not support + * copy-on-write, then the operation will fail. + * + * ```js + * import { copyFileSync, constants } from 'node:fs'; + * + * // destination.txt will be created or overwritten by default. + * copyFileSync('source.txt', 'destination.txt'); + * console.log('source.txt was copied to destination.txt'); + * + * // By using COPYFILE_EXCL, the operation will fail if destination.txt exists. + * copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL); + * ``` + * @since v8.5.0 + * @param src source filename to copy + * @param dest destination filename of the copy operation + * @param [mode=0] modifiers for copy operation. + */ + export function copyFileSync(src: PathLike, dest: PathLike, mode?: number): void; + /** + * Write an array of `ArrayBufferView`s to the file specified by `fd` using `writev()`. + * + * `position` is the offset from the beginning of the file where this data + * should be written. If `typeof position !== 'number'`, the data will be written + * at the current position. + * + * The callback will be given three arguments: `err`, `bytesWritten`, and `buffers`. `bytesWritten` is how many bytes were written from `buffers`. + * + * If this method is `util.promisify()` ed, it returns a promise for an `Object` with `bytesWritten` and `buffers` properties. + * + * It is unsafe to use `fs.writev()` multiple times on the same file without + * waiting for the callback. For this scenario, use {@link createWriteStream}. + * + * On Linux, positional writes don't work when the file is opened in append mode. + * The kernel ignores the position argument and always appends the data to + * the end of the file. + * @since v12.9.0 + * @param [position='null'] + */ + export function writev( + fd: number, + buffers: TBuffers, + cb: (err: NodeJS.ErrnoException | null, bytesWritten: number, buffers: TBuffers) => void, + ): void; + export function writev( + fd: number, + buffers: TBuffers, + position: number | null, + cb: (err: NodeJS.ErrnoException | null, bytesWritten: number, buffers: TBuffers) => void, + ): void; + // Providing a default type parameter doesn't provide true BC for userland consumers, but at least suppresses TS2314 + // TODO: remove default in future major version + export interface WriteVResult { + bytesWritten: number; + buffers: T; + } + export namespace writev { + function __promisify__( + fd: number, + buffers: TBuffers, + position?: number, + ): Promise>; + } + /** + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link writev}. + * @since v12.9.0 + * @param [position='null'] + * @return The number of bytes written. + */ + export function writevSync(fd: number, buffers: readonly NodeJS.ArrayBufferView[], position?: number): number; + /** + * Read from a file specified by `fd` and write to an array of `ArrayBufferView`s + * using `readv()`. + * + * `position` is the offset from the beginning of the file from where data + * should be read. If `typeof position !== 'number'`, the data will be read + * from the current position. + * + * The callback will be given three arguments: `err`, `bytesRead`, and `buffers`. `bytesRead` is how many bytes were read from the file. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a promise for an `Object` with `bytesRead` and `buffers` properties. + * @since v13.13.0, v12.17.0 + * @param [position='null'] + */ + export function readv( + fd: number, + buffers: TBuffers, + cb: (err: NodeJS.ErrnoException | null, bytesRead: number, buffers: TBuffers) => void, + ): void; + export function readv( + fd: number, + buffers: TBuffers, + position: number | null, + cb: (err: NodeJS.ErrnoException | null, bytesRead: number, buffers: TBuffers) => void, + ): void; + // Providing a default type parameter doesn't provide true BC for userland consumers, but at least suppresses TS2314 + // TODO: remove default in future major version + export interface ReadVResult { + bytesRead: number; + buffers: T; + } + export namespace readv { + function __promisify__( + fd: number, + buffers: TBuffers, + position?: number, + ): Promise>; + } + /** + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link readv}. + * @since v13.13.0, v12.17.0 + * @param [position='null'] + * @return The number of bytes read. + */ + export function readvSync(fd: number, buffers: readonly NodeJS.ArrayBufferView[], position?: number): number; + + export interface OpenAsBlobOptions { + /** + * An optional mime type for the blob. + * + * @default 'undefined' + */ + type?: string | undefined; + } + + /** + * Returns a `Blob` whose data is backed by the given file. + * + * The file must not be modified after the `Blob` is created. Any modifications + * will cause reading the `Blob` data to fail with a `DOMException` error. + * Synchronous stat operations on the file when the `Blob` is created, and before + * each read in order to detect whether the file data has been modified on disk. + * + * ```js + * import { openAsBlob } from 'node:fs'; + * + * const blob = await openAsBlob('the.file.txt'); + * const ab = await blob.arrayBuffer(); + * blob.stream(); + * ``` + * @since v19.8.0 + */ + export function openAsBlob(path: PathLike, options?: OpenAsBlobOptions): Promise; + + export interface OpenDirOptions { + /** + * @default 'utf8' + */ + encoding?: BufferEncoding | undefined; + /** + * Number of directory entries that are buffered + * internally when reading from the directory. Higher values lead to better + * performance but higher memory usage. + * @default 32 + */ + bufferSize?: number | undefined; + /** + * @default false + */ + recursive?: boolean | undefined; + } + /** + * Synchronously open a directory. See [`opendir(3)`](http://man7.org/linux/man-pages/man3/opendir.3.html). + * + * Creates an `fs.Dir`, which contains all further functions for reading from + * and cleaning up the directory. + * + * The `encoding` option sets the encoding for the `path` while opening the + * directory and subsequent read operations. + * @since v12.12.0 + */ + export function opendirSync(path: PathLike, options?: OpenDirOptions): Dir; + /** + * Asynchronously open a directory. See the POSIX [`opendir(3)`](http://man7.org/linux/man-pages/man3/opendir.3.html) documentation for + * more details. + * + * Creates an `fs.Dir`, which contains all further functions for reading from + * and cleaning up the directory. + * + * The `encoding` option sets the encoding for the `path` while opening the + * directory and subsequent read operations. + * @since v12.12.0 + */ + export function opendir(path: PathLike, cb: (err: NodeJS.ErrnoException | null, dir: Dir) => void): void; + export function opendir( + path: PathLike, + options: OpenDirOptions, + cb: (err: NodeJS.ErrnoException | null, dir: Dir) => void, + ): void; + export namespace opendir { + function __promisify__(path: PathLike, options?: OpenDirOptions): Promise; + } + export interface BigIntStats extends StatsBase { + atimeNs: bigint; + mtimeNs: bigint; + ctimeNs: bigint; + birthtimeNs: bigint; + } + export interface BigIntOptions { + bigint: true; + } + export interface StatOptions { + bigint?: boolean | undefined; + } + export interface StatSyncOptions extends StatOptions { + throwIfNoEntry?: boolean | undefined; + } + interface CopyOptionsBase { + /** + * Dereference symlinks + * @default false + */ + dereference?: boolean | undefined; + /** + * When `force` is `false`, and the destination + * exists, throw an error. + * @default false + */ + errorOnExist?: boolean | undefined; + /** + * Overwrite existing file or directory. _The copy + * operation will ignore errors if you set this to false and the destination + * exists. Use the `errorOnExist` option to change this behavior. + * @default true + */ + force?: boolean | undefined; + /** + * Modifiers for copy operation. See `mode` flag of {@link copyFileSync()} + */ + mode?: number | undefined; + /** + * When `true` timestamps from `src` will + * be preserved. + * @default false + */ + preserveTimestamps?: boolean | undefined; + /** + * Copy directories recursively. + * @default false + */ + recursive?: boolean | undefined; + /** + * When true, path resolution for symlinks will be skipped + * @default false + */ + verbatimSymlinks?: boolean | undefined; + } + export interface CopyOptions extends CopyOptionsBase { + /** + * Function to filter copied files/directories. Return + * `true` to copy the item, `false` to ignore it. + */ + filter?: ((source: string, destination: string) => boolean | Promise) | undefined; + } + export interface CopySyncOptions extends CopyOptionsBase { + /** + * Function to filter copied files/directories. Return + * `true` to copy the item, `false` to ignore it. + */ + filter?: ((source: string, destination: string) => boolean) | undefined; + } + /** + * Asynchronously copies the entire directory structure from `src` to `dest`, + * including subdirectories and files. + * + * When copying a directory to another directory, globs are not supported and + * behavior is similar to `cp dir1/ dir2/`. + * @since v16.7.0 + * @experimental + * @param src source path to copy. + * @param dest destination path to copy to. + */ + export function cp( + source: string | URL, + destination: string | URL, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function cp( + source: string | URL, + destination: string | URL, + opts: CopyOptions, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + /** + * Synchronously copies the entire directory structure from `src` to `dest`, + * including subdirectories and files. + * + * When copying a directory to another directory, globs are not supported and + * behavior is similar to `cp dir1/ dir2/`. + * @since v16.7.0 + * @experimental + * @param src source path to copy. + * @param dest destination path to copy to. + */ + export function cpSync(source: string | URL, destination: string | URL, opts?: CopySyncOptions): void; + + interface _GlobOptions { + /** + * Current working directory. + * @default process.cwd() + */ + cwd?: string | URL | undefined; + /** + * `true` if the glob should return paths as `Dirent`s, `false` otherwise. + * @default false + * @since v22.2.0 + */ + withFileTypes?: boolean | undefined; + /** + * Function to filter out files/directories or a + * list of glob patterns to be excluded. If a function is provided, return + * `true` to exclude the item, `false` to include it. + * @default undefined + */ + exclude?: ((fileName: T) => boolean) | readonly string[] | undefined; + } + export interface GlobOptions extends _GlobOptions {} + export interface GlobOptionsWithFileTypes extends _GlobOptions { + withFileTypes: true; + } + export interface GlobOptionsWithoutFileTypes extends _GlobOptions { + withFileTypes?: false | undefined; + } + + /** + * Retrieves the files matching the specified pattern. + * + * ```js + * import { glob } from 'node:fs'; + * + * glob('*.js', (err, matches) => { + * if (err) throw err; + * console.log(matches); + * }); + * ``` + * @since v22.0.0 + */ + export function glob( + pattern: string | readonly string[], + callback: (err: NodeJS.ErrnoException | null, matches: string[]) => void, + ): void; + export function glob( + pattern: string | readonly string[], + options: GlobOptionsWithFileTypes, + callback: ( + err: NodeJS.ErrnoException | null, + matches: Dirent[], + ) => void, + ): void; + export function glob( + pattern: string | readonly string[], + options: GlobOptionsWithoutFileTypes, + callback: ( + err: NodeJS.ErrnoException | null, + matches: string[], + ) => void, + ): void; + export function glob( + pattern: string | readonly string[], + options: GlobOptions, + callback: ( + err: NodeJS.ErrnoException | null, + matches: Dirent[] | string[], + ) => void, + ): void; + /** + * ```js + * import { globSync } from 'node:fs'; + * + * console.log(globSync('*.js')); + * ``` + * @since v22.0.0 + * @returns paths of files that match the pattern. + */ + export function globSync(pattern: string | readonly string[]): string[]; + export function globSync( + pattern: string | readonly string[], + options: GlobOptionsWithFileTypes, + ): Dirent[]; + export function globSync( + pattern: string | readonly string[], + options: GlobOptionsWithoutFileTypes, + ): string[]; + export function globSync( + pattern: string | readonly string[], + options: GlobOptions, + ): Dirent[] | string[]; +} +declare module "node:fs" { + export * from "fs"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/fs/promises.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/fs/promises.d.ts new file mode 100644 index 00000000..051ddba4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/fs/promises.d.ts @@ -0,0 +1,1295 @@ +/** + * The `fs/promises` API provides asynchronous file system methods that return + * promises. + * + * The promise APIs use the underlying Node.js threadpool to perform file + * system operations off the event loop thread. These operations are not + * synchronized or threadsafe. Care must be taken when performing multiple + * concurrent modifications on the same file or data corruption may occur. + * @since v10.0.0 + */ +declare module "fs/promises" { + import { NonSharedBuffer } from "node:buffer"; + import { Abortable } from "node:events"; + import { Stream } from "node:stream"; + import { ReadableStream } from "node:stream/web"; + import { + BigIntStats, + BigIntStatsFs, + BufferEncodingOption, + constants as fsConstants, + CopyOptions, + Dir, + Dirent, + GlobOptions, + GlobOptionsWithFileTypes, + GlobOptionsWithoutFileTypes, + MakeDirectoryOptions, + Mode, + ObjectEncodingOptions, + OpenDirOptions, + OpenMode, + PathLike, + ReadOptions, + ReadOptionsWithBuffer, + ReadPosition, + ReadStream, + ReadVResult, + RmDirOptions, + RmOptions, + StatFsOptions, + StatOptions, + Stats, + StatsFs, + TimeLike, + WatchEventType, + WatchOptions as _WatchOptions, + WriteStream, + WriteVResult, + } from "node:fs"; + import { Interface as ReadlineInterface } from "node:readline"; + interface FileChangeInfo { + eventType: WatchEventType; + filename: T | null; + } + interface FlagAndOpenMode { + mode?: Mode | undefined; + flag?: OpenMode | undefined; + } + interface FileReadResult { + bytesRead: number; + buffer: T; + } + /** @deprecated This interface will be removed in a future version. Use `import { ReadOptionsWithBuffer } from "node:fs"` instead. */ + interface FileReadOptions { + /** + * @default `Buffer.alloc(0xffff)` + */ + buffer?: T; + /** + * @default 0 + */ + offset?: number | null; + /** + * @default `buffer.byteLength` + */ + length?: number | null; + position?: ReadPosition | null; + } + interface CreateReadStreamOptions extends Abortable { + encoding?: BufferEncoding | null | undefined; + autoClose?: boolean | undefined; + emitClose?: boolean | undefined; + start?: number | undefined; + end?: number | undefined; + highWaterMark?: number | undefined; + } + interface CreateWriteStreamOptions { + encoding?: BufferEncoding | null | undefined; + autoClose?: boolean | undefined; + emitClose?: boolean | undefined; + start?: number | undefined; + highWaterMark?: number | undefined; + flush?: boolean | undefined; + } + interface ReadableWebStreamOptions { + autoClose?: boolean | undefined; + } + // TODO: Add `EventEmitter` close + interface FileHandle { + /** + * The numeric file descriptor managed by the {FileHandle} object. + * @since v10.0.0 + */ + readonly fd: number; + /** + * Alias of `filehandle.writeFile()`. + * + * When operating on file handles, the mode cannot be changed from what it was set + * to with `fsPromises.open()`. Therefore, this is equivalent to `filehandle.writeFile()`. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + appendFile( + data: string | Uint8Array, + options?: + | (ObjectEncodingOptions & Abortable) + | BufferEncoding + | null, + ): Promise; + /** + * Changes the ownership of the file. A wrapper for [`chown(2)`](http://man7.org/linux/man-pages/man2/chown.2.html). + * @since v10.0.0 + * @param uid The file's new owner's user id. + * @param gid The file's new group's group id. + * @return Fulfills with `undefined` upon success. + */ + chown(uid: number, gid: number): Promise; + /** + * Modifies the permissions on the file. See [`chmod(2)`](http://man7.org/linux/man-pages/man2/chmod.2.html). + * @since v10.0.0 + * @param mode the file mode bit mask. + * @return Fulfills with `undefined` upon success. + */ + chmod(mode: Mode): Promise; + /** + * Unlike the 16 KiB default `highWaterMark` for a `stream.Readable`, the stream + * returned by this method has a default `highWaterMark` of 64 KiB. + * + * `options` can include `start` and `end` values to read a range of bytes from + * the file instead of the entire file. Both `start` and `end` are inclusive and + * start counting at 0, allowed values are in the + * \[0, [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)\] range. If `start` is + * omitted or `undefined`, `filehandle.createReadStream()` reads sequentially from + * the current file position. The `encoding` can be any one of those accepted by `Buffer`. + * + * If the `FileHandle` points to a character device that only supports blocking + * reads (such as keyboard or sound card), read operations do not finish until data + * is available. This can prevent the process from exiting and the stream from + * closing naturally. + * + * By default, the stream will emit a `'close'` event after it has been + * destroyed. Set the `emitClose` option to `false` to change this behavior. + * + * ```js + * import { open } from 'node:fs/promises'; + * + * const fd = await open('/dev/input/event0'); + * // Create a stream from some character device. + * const stream = fd.createReadStream(); + * setTimeout(() => { + * stream.close(); // This may not close the stream. + * // Artificially marking end-of-stream, as if the underlying resource had + * // indicated end-of-file by itself, allows the stream to close. + * // This does not cancel pending read operations, and if there is such an + * // operation, the process may still not be able to exit successfully + * // until it finishes. + * stream.push(null); + * stream.read(0); + * }, 100); + * ``` + * + * If `autoClose` is false, then the file descriptor won't be closed, even if + * there's an error. It is the application's responsibility to close it and make + * sure there's no file descriptor leak. If `autoClose` is set to true (default + * behavior), on `'error'` or `'end'` the file descriptor will be closed + * automatically. + * + * An example to read the last 10 bytes of a file which is 100 bytes long: + * + * ```js + * import { open } from 'node:fs/promises'; + * + * const fd = await open('sample.txt'); + * fd.createReadStream({ start: 90, end: 99 }); + * ``` + * @since v16.11.0 + */ + createReadStream(options?: CreateReadStreamOptions): ReadStream; + /** + * `options` may also include a `start` option to allow writing data at some + * position past the beginning of the file, allowed values are in the + * \[0, [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)\] range. Modifying a file rather than + * replacing it may require the `flags` `open` option to be set to `r+` rather than + * the default `r`. The `encoding` can be any one of those accepted by `Buffer`. + * + * If `autoClose` is set to true (default behavior) on `'error'` or `'finish'` the file descriptor will be closed automatically. If `autoClose` is false, + * then the file descriptor won't be closed, even if there's an error. + * It is the application's responsibility to close it and make sure there's no + * file descriptor leak. + * + * By default, the stream will emit a `'close'` event after it has been + * destroyed. Set the `emitClose` option to `false` to change this behavior. + * @since v16.11.0 + */ + createWriteStream(options?: CreateWriteStreamOptions): WriteStream; + /** + * Forces all currently queued I/O operations associated with the file to the + * operating system's synchronized I/O completion state. Refer to the POSIX [`fdatasync(2)`](http://man7.org/linux/man-pages/man2/fdatasync.2.html) documentation for details. + * + * Unlike `filehandle.sync` this method does not flush modified metadata. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + datasync(): Promise; + /** + * Request that all data for the open file descriptor is flushed to the storage + * device. The specific implementation is operating system and device specific. + * Refer to the POSIX [`fsync(2)`](http://man7.org/linux/man-pages/man2/fsync.2.html) documentation for more detail. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + sync(): Promise; + /** + * Reads data from the file and stores that in the given buffer. + * + * If the file is not modified concurrently, the end-of-file is reached when the + * number of bytes read is zero. + * @since v10.0.0 + * @param buffer A buffer that will be filled with the file data read. + * @param offset The location in the buffer at which to start filling. + * @param length The number of bytes to read. + * @param position The location where to begin reading data from the file. If `null`, data will be read from the current file position, and the position will be updated. If `position` is an + * integer, the current file position will remain unchanged. + * @return Fulfills upon success with an object with two properties: + */ + read( + buffer: T, + offset?: number | null, + length?: number | null, + position?: ReadPosition | null, + ): Promise>; + read( + buffer: T, + options?: ReadOptions, + ): Promise>; + read( + options?: ReadOptionsWithBuffer, + ): Promise>; + /** + * Returns a byte-oriented `ReadableStream` that may be used to read the file's + * contents. + * + * An error will be thrown if this method is called more than once or is called + * after the `FileHandle` is closed or closing. + * + * ```js + * import { + * open, + * } from 'node:fs/promises'; + * + * const file = await open('./some/file/to/read'); + * + * for await (const chunk of file.readableWebStream()) + * console.log(chunk); + * + * await file.close(); + * ``` + * + * While the `ReadableStream` will read the file to completion, it will not + * close the `FileHandle` automatically. User code must still call the`fileHandle.close()` method. + * @since v17.0.0 + */ + readableWebStream(options?: ReadableWebStreamOptions): ReadableStream; + /** + * Asynchronously reads the entire contents of a file. + * + * If `options` is a string, then it specifies the `encoding`. + * + * The `FileHandle` has to support reading. + * + * If one or more `filehandle.read()` calls are made on a file handle and then a `filehandle.readFile()` call is made, the data will be read from the current + * position till the end of the file. It doesn't always read from the beginning + * of the file. + * @since v10.0.0 + * @return Fulfills upon a successful read with the contents of the file. If no encoding is specified (using `options.encoding`), the data is returned as a {Buffer} object. Otherwise, the + * data will be a string. + */ + readFile( + options?: + | ({ encoding?: null | undefined } & Abortable) + | null, + ): Promise; + /** + * Asynchronously reads the entire contents of a file. The underlying file will _not_ be closed automatically. + * The `FileHandle` must have been opened for reading. + */ + readFile( + options: + | ({ encoding: BufferEncoding } & Abortable) + | BufferEncoding, + ): Promise; + /** + * Asynchronously reads the entire contents of a file. The underlying file will _not_ be closed automatically. + * The `FileHandle` must have been opened for reading. + */ + readFile( + options?: + | (ObjectEncodingOptions & Abortable) + | BufferEncoding + | null, + ): Promise; + /** + * Convenience method to create a `readline` interface and stream over the file. + * See `filehandle.createReadStream()` for the options. + * + * ```js + * import { open } from 'node:fs/promises'; + * + * const file = await open('./some/file/to/read'); + * + * for await (const line of file.readLines()) { + * console.log(line); + * } + * ``` + * @since v18.11.0 + */ + readLines(options?: CreateReadStreamOptions): ReadlineInterface; + /** + * @since v10.0.0 + * @return Fulfills with an {fs.Stats} for the file. + */ + stat( + opts?: StatOptions & { + bigint?: false | undefined; + }, + ): Promise; + stat( + opts: StatOptions & { + bigint: true; + }, + ): Promise; + stat(opts?: StatOptions): Promise; + /** + * Truncates the file. + * + * If the file was larger than `len` bytes, only the first `len` bytes will be + * retained in the file. + * + * The following example retains only the first four bytes of the file: + * + * ```js + * import { open } from 'node:fs/promises'; + * + * let filehandle = null; + * try { + * filehandle = await open('temp.txt', 'r+'); + * await filehandle.truncate(4); + * } finally { + * await filehandle?.close(); + * } + * ``` + * + * If the file previously was shorter than `len` bytes, it is extended, and the + * extended part is filled with null bytes (`'\0'`): + * + * If `len` is negative then `0` will be used. + * @since v10.0.0 + * @param [len=0] + * @return Fulfills with `undefined` upon success. + */ + truncate(len?: number): Promise; + /** + * Change the file system timestamps of the object referenced by the `FileHandle` then fulfills the promise with no arguments upon success. + * @since v10.0.0 + */ + utimes(atime: TimeLike, mtime: TimeLike): Promise; + /** + * Asynchronously writes data to a file, replacing the file if it already exists. `data` can be a string, a buffer, an + * [AsyncIterable](https://tc39.github.io/ecma262/#sec-asynciterable-interface), or an + * [Iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol) object. + * The promise is fulfilled with no arguments upon success. + * + * If `options` is a string, then it specifies the `encoding`. + * + * The `FileHandle` has to support writing. + * + * It is unsafe to use `filehandle.writeFile()` multiple times on the same file + * without waiting for the promise to be fulfilled (or rejected). + * + * If one or more `filehandle.write()` calls are made on a file handle and then a`filehandle.writeFile()` call is made, the data will be written from the + * current position till the end of the file. It doesn't always write from the + * beginning of the file. + * @since v10.0.0 + */ + writeFile( + data: string | Uint8Array, + options?: + | (ObjectEncodingOptions & Abortable) + | BufferEncoding + | null, + ): Promise; + /** + * Write `buffer` to the file. + * + * The promise is fulfilled with an object containing two properties: + * + * It is unsafe to use `filehandle.write()` multiple times on the same file + * without waiting for the promise to be fulfilled (or rejected). For this + * scenario, use `filehandle.createWriteStream()`. + * + * On Linux, positional writes do not work when the file is opened in append mode. + * The kernel ignores the position argument and always appends the data to + * the end of the file. + * @since v10.0.0 + * @param offset The start position from within `buffer` where the data to write begins. + * @param [length=buffer.byteLength - offset] The number of bytes from `buffer` to write. + * @param [position='null'] The offset from the beginning of the file where the data from `buffer` should be written. If `position` is not a `number`, the data will be written at the current + * position. See the POSIX pwrite(2) documentation for more detail. + */ + write( + buffer: TBuffer, + offset?: number | null, + length?: number | null, + position?: number | null, + ): Promise<{ + bytesWritten: number; + buffer: TBuffer; + }>; + write( + buffer: TBuffer, + options?: { offset?: number; length?: number; position?: number }, + ): Promise<{ + bytesWritten: number; + buffer: TBuffer; + }>; + write( + data: string, + position?: number | null, + encoding?: BufferEncoding | null, + ): Promise<{ + bytesWritten: number; + buffer: string; + }>; + /** + * Write an array of [ArrayBufferView](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView) s to the file. + * + * The promise is fulfilled with an object containing a two properties: + * + * It is unsafe to call `writev()` multiple times on the same file without waiting + * for the promise to be fulfilled (or rejected). + * + * On Linux, positional writes don't work when the file is opened in append mode. + * The kernel ignores the position argument and always appends the data to + * the end of the file. + * @since v12.9.0 + * @param [position='null'] The offset from the beginning of the file where the data from `buffers` should be written. If `position` is not a `number`, the data will be written at the current + * position. + */ + writev( + buffers: TBuffers, + position?: number, + ): Promise>; + /** + * Read from a file and write to an array of [ArrayBufferView](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView) s + * @since v13.13.0, v12.17.0 + * @param [position='null'] The offset from the beginning of the file where the data should be read from. If `position` is not a `number`, the data will be read from the current position. + * @return Fulfills upon success an object containing two properties: + */ + readv( + buffers: TBuffers, + position?: number, + ): Promise>; + /** + * Closes the file handle after waiting for any pending operation on the handle to + * complete. + * + * ```js + * import { open } from 'node:fs/promises'; + * + * let filehandle; + * try { + * filehandle = await open('thefile.txt', 'r'); + * } finally { + * await filehandle?.close(); + * } + * ``` + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + close(): Promise; + /** + * Calls `filehandle.close()` and returns a promise that fulfills when the + * filehandle is closed. + * @since v20.4.0 + */ + [Symbol.asyncDispose](): Promise; + } + const constants: typeof fsConstants; + /** + * Tests a user's permissions for the file or directory specified by `path`. + * The `mode` argument is an optional integer that specifies the accessibility + * checks to be performed. `mode` should be either the value `fs.constants.F_OK` or a mask consisting of the bitwise OR of any of `fs.constants.R_OK`, `fs.constants.W_OK`, and `fs.constants.X_OK` + * (e.g.`fs.constants.W_OK | fs.constants.R_OK`). Check `File access constants` for + * possible values of `mode`. + * + * If the accessibility check is successful, the promise is fulfilled with no + * value. If any of the accessibility checks fail, the promise is rejected + * with an [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) object. The following example checks if the file`/etc/passwd` can be read and + * written by the current process. + * + * ```js + * import { access, constants } from 'node:fs/promises'; + * + * try { + * await access('/etc/passwd', constants.R_OK | constants.W_OK); + * console.log('can access'); + * } catch { + * console.error('cannot access'); + * } + * ``` + * + * Using `fsPromises.access()` to check for the accessibility of a file before + * calling `fsPromises.open()` is not recommended. Doing so introduces a race + * condition, since other processes may change the file's state between the two + * calls. Instead, user code should open/read/write the file directly and handle + * the error raised if the file is not accessible. + * @since v10.0.0 + * @param [mode=fs.constants.F_OK] + * @return Fulfills with `undefined` upon success. + */ + function access(path: PathLike, mode?: number): Promise; + /** + * Asynchronously copies `src` to `dest`. By default, `dest` is overwritten if it + * already exists. + * + * No guarantees are made about the atomicity of the copy operation. If an + * error occurs after the destination file has been opened for writing, an attempt + * will be made to remove the destination. + * + * ```js + * import { copyFile, constants } from 'node:fs/promises'; + * + * try { + * await copyFile('source.txt', 'destination.txt'); + * console.log('source.txt was copied to destination.txt'); + * } catch { + * console.error('The file could not be copied'); + * } + * + * // By using COPYFILE_EXCL, the operation will fail if destination.txt exists. + * try { + * await copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL); + * console.log('source.txt was copied to destination.txt'); + * } catch { + * console.error('The file could not be copied'); + * } + * ``` + * @since v10.0.0 + * @param src source filename to copy + * @param dest destination filename of the copy operation + * @param [mode=0] Optional modifiers that specify the behavior of the copy operation. It is possible to create a mask consisting of the bitwise OR of two or more values (e.g. + * `fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE`) + * @return Fulfills with `undefined` upon success. + */ + function copyFile(src: PathLike, dest: PathLike, mode?: number): Promise; + /** + * Opens a `FileHandle`. + * + * Refer to the POSIX [`open(2)`](http://man7.org/linux/man-pages/man2/open.2.html) documentation for more detail. + * + * Some characters (`< > : " / \ | ? *`) are reserved under Windows as documented + * by [Naming Files, Paths, and Namespaces](https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file). Under NTFS, if the filename contains + * a colon, Node.js will open a file system stream, as described by [this MSDN page](https://docs.microsoft.com/en-us/windows/desktop/FileIO/using-streams). + * @since v10.0.0 + * @param [flags='r'] See `support of file system `flags``. + * @param [mode=0o666] Sets the file mode (permission and sticky bits) if the file is created. + * @return Fulfills with a {FileHandle} object. + */ + function open(path: PathLike, flags?: string | number, mode?: Mode): Promise; + /** + * Renames `oldPath` to `newPath`. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function rename(oldPath: PathLike, newPath: PathLike): Promise; + /** + * Truncates (shortens or extends the length) of the content at `path` to `len` bytes. + * @since v10.0.0 + * @param [len=0] + * @return Fulfills with `undefined` upon success. + */ + function truncate(path: PathLike, len?: number): Promise; + /** + * Removes the directory identified by `path`. + * + * Using `fsPromises.rmdir()` on a file (not a directory) results in the + * promise being rejected with an `ENOENT` error on Windows and an `ENOTDIR` error on POSIX. + * + * To get a behavior similar to the `rm -rf` Unix command, use `fsPromises.rm()` with options `{ recursive: true, force: true }`. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function rmdir(path: PathLike, options?: RmDirOptions): Promise; + /** + * Removes files and directories (modeled on the standard POSIX `rm` utility). + * @since v14.14.0 + * @return Fulfills with `undefined` upon success. + */ + function rm(path: PathLike, options?: RmOptions): Promise; + /** + * Asynchronously creates a directory. + * + * The optional `options` argument can be an integer specifying `mode` (permission + * and sticky bits), or an object with a `mode` property and a `recursive` property indicating whether parent directories should be created. Calling `fsPromises.mkdir()` when `path` is a directory + * that exists results in a + * rejection only when `recursive` is false. + * + * ```js + * import { mkdir } from 'node:fs/promises'; + * + * try { + * const projectFolder = new URL('./test/project/', import.meta.url); + * const createDir = await mkdir(projectFolder, { recursive: true }); + * + * console.log(`created ${createDir}`); + * } catch (err) { + * console.error(err.message); + * } + * ``` + * @since v10.0.0 + * @return Upon success, fulfills with `undefined` if `recursive` is `false`, or the first directory path created if `recursive` is `true`. + */ + function mkdir( + path: PathLike, + options: MakeDirectoryOptions & { + recursive: true; + }, + ): Promise; + /** + * Asynchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + function mkdir( + path: PathLike, + options?: + | Mode + | (MakeDirectoryOptions & { + recursive?: false | undefined; + }) + | null, + ): Promise; + /** + * Asynchronous mkdir(2) - create a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders + * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. + */ + function mkdir(path: PathLike, options?: Mode | MakeDirectoryOptions | null): Promise; + /** + * Reads the contents of a directory. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the filenames. If the `encoding` is set to `'buffer'`, the filenames returned + * will be passed as `Buffer` objects. + * + * If `options.withFileTypes` is set to `true`, the returned array will contain `fs.Dirent` objects. + * + * ```js + * import { readdir } from 'node:fs/promises'; + * + * try { + * const files = await readdir(path); + * for (const file of files) + * console.log(file); + * } catch (err) { + * console.error(err); + * } + * ``` + * @since v10.0.0 + * @return Fulfills with an array of the names of the files in the directory excluding `'.'` and `'..'`. + */ + function readdir( + path: PathLike, + options?: + | (ObjectEncodingOptions & { + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + }) + | BufferEncoding + | null, + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function readdir( + path: PathLike, + options: + | { + encoding: "buffer"; + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + } + | "buffer", + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function readdir( + path: PathLike, + options?: + | (ObjectEncodingOptions & { + withFileTypes?: false | undefined; + recursive?: boolean | undefined; + }) + | BufferEncoding + | null, + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options If called with `withFileTypes: true` the result data will be an array of Dirent. + */ + function readdir( + path: PathLike, + options: ObjectEncodingOptions & { + withFileTypes: true; + recursive?: boolean | undefined; + }, + ): Promise; + /** + * Asynchronous readdir(3) - read a directory. + * @param path A path to a directory. If a URL is provided, it must use the `file:` protocol. + * @param options Must include `withFileTypes: true` and `encoding: 'buffer'`. + */ + function readdir( + path: PathLike, + options: { + encoding: "buffer"; + withFileTypes: true; + recursive?: boolean | undefined; + }, + ): Promise[]>; + /** + * Reads the contents of the symbolic link referred to by `path`. See the POSIX [`readlink(2)`](http://man7.org/linux/man-pages/man2/readlink.2.html) documentation for more detail. The promise is + * fulfilled with the`linkString` upon success. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the link path returned. If the `encoding` is set to `'buffer'`, the link path + * returned will be passed as a `Buffer` object. + * @since v10.0.0 + * @return Fulfills with the `linkString` upon success. + */ + function readlink(path: PathLike, options?: ObjectEncodingOptions | BufferEncoding | null): Promise; + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function readlink(path: PathLike, options: BufferEncodingOption): Promise; + /** + * Asynchronous readlink(2) - read value of a symbolic link. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function readlink( + path: PathLike, + options?: ObjectEncodingOptions | string | null, + ): Promise; + /** + * Creates a symbolic link. + * + * The `type` argument is only used on Windows platforms and can be one of `'dir'`, `'file'`, or `'junction'`. If the `type` argument is not a string, Node.js will + * autodetect `target` type and use `'file'` or `'dir'`. If the `target` does not + * exist, `'file'` will be used. Windows junction points require the destination + * path to be absolute. When using `'junction'`, the `target` argument will + * automatically be normalized to absolute path. Junction points on NTFS volumes + * can only point to directories. + * @since v10.0.0 + * @param [type='null'] + * @return Fulfills with `undefined` upon success. + */ + function symlink(target: PathLike, path: PathLike, type?: string | null): Promise; + /** + * Equivalent to `fsPromises.stat()` unless `path` refers to a symbolic link, + * in which case the link itself is stat-ed, not the file that it refers to. + * Refer to the POSIX [`lstat(2)`](http://man7.org/linux/man-pages/man2/lstat.2.html) document for more detail. + * @since v10.0.0 + * @return Fulfills with the {fs.Stats} object for the given symbolic link `path`. + */ + function lstat( + path: PathLike, + opts?: StatOptions & { + bigint?: false | undefined; + }, + ): Promise; + function lstat( + path: PathLike, + opts: StatOptions & { + bigint: true; + }, + ): Promise; + function lstat(path: PathLike, opts?: StatOptions): Promise; + /** + * @since v10.0.0 + * @return Fulfills with the {fs.Stats} object for the given `path`. + */ + function stat( + path: PathLike, + opts?: StatOptions & { + bigint?: false | undefined; + }, + ): Promise; + function stat( + path: PathLike, + opts: StatOptions & { + bigint: true; + }, + ): Promise; + function stat(path: PathLike, opts?: StatOptions): Promise; + /** + * @since v19.6.0, v18.15.0 + * @return Fulfills with the {fs.StatFs} object for the given `path`. + */ + function statfs( + path: PathLike, + opts?: StatFsOptions & { + bigint?: false | undefined; + }, + ): Promise; + function statfs( + path: PathLike, + opts: StatFsOptions & { + bigint: true; + }, + ): Promise; + function statfs(path: PathLike, opts?: StatFsOptions): Promise; + /** + * Creates a new link from the `existingPath` to the `newPath`. See the POSIX [`link(2)`](http://man7.org/linux/man-pages/man2/link.2.html) documentation for more detail. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function link(existingPath: PathLike, newPath: PathLike): Promise; + /** + * If `path` refers to a symbolic link, then the link is removed without affecting + * the file or directory to which that link refers. If the `path` refers to a file + * path that is not a symbolic link, the file is deleted. See the POSIX [`unlink(2)`](http://man7.org/linux/man-pages/man2/unlink.2.html) documentation for more detail. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function unlink(path: PathLike): Promise; + /** + * Changes the permissions of a file. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function chmod(path: PathLike, mode: Mode): Promise; + /** + * Changes the permissions on a symbolic link. + * + * This method is only implemented on macOS. + * @deprecated Since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function lchmod(path: PathLike, mode: Mode): Promise; + /** + * Changes the ownership on a symbolic link. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function lchown(path: PathLike, uid: number, gid: number): Promise; + /** + * Changes the access and modification times of a file in the same way as `fsPromises.utimes()`, with the difference that if the path refers to a + * symbolic link, then the link is not dereferenced: instead, the timestamps of + * the symbolic link itself are changed. + * @since v14.5.0, v12.19.0 + * @return Fulfills with `undefined` upon success. + */ + function lutimes(path: PathLike, atime: TimeLike, mtime: TimeLike): Promise; + /** + * Changes the ownership of a file. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function chown(path: PathLike, uid: number, gid: number): Promise; + /** + * Change the file system timestamps of the object referenced by `path`. + * + * The `atime` and `mtime` arguments follow these rules: + * + * * Values can be either numbers representing Unix epoch time, `Date`s, or a + * numeric string like `'123456789.0'`. + * * If the value can not be converted to a number, or is `NaN`, `Infinity`, or `-Infinity`, an `Error` will be thrown. + * @since v10.0.0 + * @return Fulfills with `undefined` upon success. + */ + function utimes(path: PathLike, atime: TimeLike, mtime: TimeLike): Promise; + /** + * Determines the actual location of `path` using the same semantics as the `fs.realpath.native()` function. + * + * Only paths that can be converted to UTF8 strings are supported. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use for + * the path. If the `encoding` is set to `'buffer'`, the path returned will be + * passed as a `Buffer` object. + * + * On Linux, when Node.js is linked against musl libc, the procfs file system must + * be mounted on `/proc` in order for this function to work. Glibc does not have + * this restriction. + * @since v10.0.0 + * @return Fulfills with the resolved path upon success. + */ + function realpath(path: PathLike, options?: ObjectEncodingOptions | BufferEncoding | null): Promise; + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function realpath(path: PathLike, options: BufferEncodingOption): Promise; + /** + * Asynchronous realpath(3) - return the canonicalized absolute pathname. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function realpath( + path: PathLike, + options?: ObjectEncodingOptions | BufferEncoding | null, + ): Promise; + /** + * Creates a unique temporary directory. A unique directory name is generated by + * appending six random characters to the end of the provided `prefix`. Due to + * platform inconsistencies, avoid trailing `X` characters in `prefix`. Some + * platforms, notably the BSDs, can return more than six random characters, and + * replace trailing `X` characters in `prefix` with random characters. + * + * The optional `options` argument can be a string specifying an encoding, or an + * object with an `encoding` property specifying the character encoding to use. + * + * ```js + * import { mkdtemp } from 'node:fs/promises'; + * import { join } from 'node:path'; + * import { tmpdir } from 'node:os'; + * + * try { + * await mkdtemp(join(tmpdir(), 'foo-')); + * } catch (err) { + * console.error(err); + * } + * ``` + * + * The `fsPromises.mkdtemp()` method will append the six randomly selected + * characters directly to the `prefix` string. For instance, given a directory `/tmp`, if the intention is to create a temporary directory _within_ `/tmp`, the `prefix` must end with a trailing + * platform-specific path separator + * (`import { sep } from 'node:path'`). + * @since v10.0.0 + * @return Fulfills with a string containing the file system path of the newly created temporary directory. + */ + function mkdtemp(prefix: string, options?: ObjectEncodingOptions | BufferEncoding | null): Promise; + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required `prefix` to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function mkdtemp(prefix: string, options: BufferEncodingOption): Promise; + /** + * Asynchronously creates a unique temporary directory. + * Generates six random characters to be appended behind a required `prefix` to create a unique temporary directory. + * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. + */ + function mkdtemp( + prefix: string, + options?: ObjectEncodingOptions | BufferEncoding | null, + ): Promise; + /** + * Asynchronously writes data to a file, replacing the file if it already exists. `data` can be a string, a buffer, an + * [AsyncIterable](https://tc39.github.io/ecma262/#sec-asynciterable-interface), or an + * [Iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol) object. + * + * The `encoding` option is ignored if `data` is a buffer. + * + * If `options` is a string, then it specifies the encoding. + * + * The `mode` option only affects the newly created file. See `fs.open()` for more details. + * + * Any specified `FileHandle` has to support writing. + * + * It is unsafe to use `fsPromises.writeFile()` multiple times on the same file + * without waiting for the promise to be settled. + * + * Similarly to `fsPromises.readFile` \- `fsPromises.writeFile` is a convenience + * method that performs multiple `write` calls internally to write the buffer + * passed to it. For performance sensitive code consider using `fs.createWriteStream()` or `filehandle.createWriteStream()`. + * + * It is possible to use an `AbortSignal` to cancel an `fsPromises.writeFile()`. + * Cancelation is "best effort", and some amount of data is likely still + * to be written. + * + * ```js + * import { writeFile } from 'node:fs/promises'; + * import { Buffer } from 'node:buffer'; + * + * try { + * const controller = new AbortController(); + * const { signal } = controller; + * const data = new Uint8Array(Buffer.from('Hello Node.js')); + * const promise = writeFile('message.txt', data, { signal }); + * + * // Abort the request before the promise settles. + * controller.abort(); + * + * await promise; + * } catch (err) { + * // When a request is aborted - err is an AbortError + * console.error(err); + * } + * ``` + * + * Aborting an ongoing request does not abort individual operating + * system requests but rather the internal buffering `fs.writeFile` performs. + * @since v10.0.0 + * @param file filename or `FileHandle` + * @return Fulfills with `undefined` upon success. + */ + function writeFile( + file: PathLike | FileHandle, + data: + | string + | NodeJS.ArrayBufferView + | Iterable + | AsyncIterable + | Stream, + options?: + | (ObjectEncodingOptions & { + mode?: Mode | undefined; + flag?: OpenMode | undefined; + /** + * If all data is successfully written to the file, and `flush` + * is `true`, `filehandle.sync()` is used to flush the data. + * @default false + */ + flush?: boolean | undefined; + } & Abortable) + | BufferEncoding + | null, + ): Promise; + /** + * Asynchronously append data to a file, creating the file if it does not yet + * exist. `data` can be a string or a `Buffer`. + * + * If `options` is a string, then it specifies the `encoding`. + * + * The `mode` option only affects the newly created file. See `fs.open()` for more details. + * + * The `path` may be specified as a `FileHandle` that has been opened + * for appending (using `fsPromises.open()`). + * @since v10.0.0 + * @param path filename or {FileHandle} + * @return Fulfills with `undefined` upon success. + */ + function appendFile( + path: PathLike | FileHandle, + data: string | Uint8Array, + options?: (ObjectEncodingOptions & FlagAndOpenMode & { flush?: boolean | undefined }) | BufferEncoding | null, + ): Promise; + /** + * Asynchronously reads the entire contents of a file. + * + * If no encoding is specified (using `options.encoding`), the data is returned + * as a `Buffer` object. Otherwise, the data will be a string. + * + * If `options` is a string, then it specifies the encoding. + * + * When the `path` is a directory, the behavior of `fsPromises.readFile()` is + * platform-specific. On macOS, Linux, and Windows, the promise will be rejected + * with an error. On FreeBSD, a representation of the directory's contents will be + * returned. + * + * An example of reading a `package.json` file located in the same directory of the + * running code: + * + * ```js + * import { readFile } from 'node:fs/promises'; + * try { + * const filePath = new URL('./package.json', import.meta.url); + * const contents = await readFile(filePath, { encoding: 'utf8' }); + * console.log(contents); + * } catch (err) { + * console.error(err.message); + * } + * ``` + * + * It is possible to abort an ongoing `readFile` using an `AbortSignal`. If a + * request is aborted the promise returned is rejected with an `AbortError`: + * + * ```js + * import { readFile } from 'node:fs/promises'; + * + * try { + * const controller = new AbortController(); + * const { signal } = controller; + * const promise = readFile(fileName, { signal }); + * + * // Abort the request before the promise settles. + * controller.abort(); + * + * await promise; + * } catch (err) { + * // When a request is aborted - err is an AbortError + * console.error(err); + * } + * ``` + * + * Aborting an ongoing request does not abort individual operating + * system requests but rather the internal buffering `fs.readFile` performs. + * + * Any specified `FileHandle` has to support reading. + * @since v10.0.0 + * @param path filename or `FileHandle` + * @return Fulfills with the contents of the file. + */ + function readFile( + path: PathLike | FileHandle, + options?: + | ({ + encoding?: null | undefined; + flag?: OpenMode | undefined; + } & Abortable) + | null, + ): Promise; + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a `FileHandle` is provided, the underlying file will _not_ be closed automatically. + * @param options An object that may contain an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + function readFile( + path: PathLike | FileHandle, + options: + | ({ + encoding: BufferEncoding; + flag?: OpenMode | undefined; + } & Abortable) + | BufferEncoding, + ): Promise; + /** + * Asynchronously reads the entire contents of a file. + * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. + * If a `FileHandle` is provided, the underlying file will _not_ be closed automatically. + * @param options An object that may contain an optional flag. + * If a flag is not provided, it defaults to `'r'`. + */ + function readFile( + path: PathLike | FileHandle, + options?: + | ( + & ObjectEncodingOptions + & Abortable + & { + flag?: OpenMode | undefined; + } + ) + | BufferEncoding + | null, + ): Promise; + /** + * Asynchronously open a directory for iterative scanning. See the POSIX [`opendir(3)`](http://man7.org/linux/man-pages/man3/opendir.3.html) documentation for more detail. + * + * Creates an `fs.Dir`, which contains all further functions for reading from + * and cleaning up the directory. + * + * The `encoding` option sets the encoding for the `path` while opening the + * directory and subsequent read operations. + * + * Example using async iteration: + * + * ```js + * import { opendir } from 'node:fs/promises'; + * + * try { + * const dir = await opendir('./'); + * for await (const dirent of dir) + * console.log(dirent.name); + * } catch (err) { + * console.error(err); + * } + * ``` + * + * When using the async iterator, the `fs.Dir` object will be automatically + * closed after the iterator exits. + * @since v12.12.0 + * @return Fulfills with an {fs.Dir}. + */ + function opendir(path: PathLike, options?: OpenDirOptions): Promise; + interface WatchOptions extends _WatchOptions { + maxQueue?: number | undefined; + overflow?: "ignore" | "throw" | undefined; + } + interface WatchOptionsWithBufferEncoding extends WatchOptions { + encoding: "buffer"; + } + interface WatchOptionsWithStringEncoding extends WatchOptions { + encoding?: BufferEncoding | undefined; + } + /** + * Returns an async iterator that watches for changes on `filename`, where `filename`is either a file or a directory. + * + * ```js + * import { watch } from 'node:fs/promises'; + * + * const ac = new AbortController(); + * const { signal } = ac; + * setTimeout(() => ac.abort(), 10000); + * + * (async () => { + * try { + * const watcher = watch(__filename, { signal }); + * for await (const event of watcher) + * console.log(event); + * } catch (err) { + * if (err.name === 'AbortError') + * return; + * throw err; + * } + * })(); + * ``` + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * All the `caveats` for `fs.watch()` also apply to `fsPromises.watch()`. + * @since v15.9.0, v14.18.0 + * @return of objects with the properties: + */ + function watch( + filename: PathLike, + options?: WatchOptionsWithStringEncoding | BufferEncoding, + ): NodeJS.AsyncIterator>; + function watch( + filename: PathLike, + options: WatchOptionsWithBufferEncoding | "buffer", + ): NodeJS.AsyncIterator>; + function watch( + filename: PathLike, + options: WatchOptions | BufferEncoding | "buffer", + ): NodeJS.AsyncIterator>; + /** + * Asynchronously copies the entire directory structure from `src` to `dest`, + * including subdirectories and files. + * + * When copying a directory to another directory, globs are not supported and + * behavior is similar to `cp dir1/ dir2/`. + * @since v16.7.0 + * @experimental + * @param src source path to copy. + * @param dest destination path to copy to. + * @return Fulfills with `undefined` upon success. + */ + function cp(source: string | URL, destination: string | URL, opts?: CopyOptions): Promise; + /** + * ```js + * import { glob } from 'node:fs/promises'; + * + * for await (const entry of glob('*.js')) + * console.log(entry); + * ``` + * @since v22.0.0 + * @returns An AsyncIterator that yields the paths of files + * that match the pattern. + */ + function glob(pattern: string | readonly string[]): NodeJS.AsyncIterator; + function glob( + pattern: string | readonly string[], + options: GlobOptionsWithFileTypes, + ): NodeJS.AsyncIterator; + function glob( + pattern: string | readonly string[], + options: GlobOptionsWithoutFileTypes, + ): NodeJS.AsyncIterator; + function glob( + pattern: string | readonly string[], + options: GlobOptions, + ): NodeJS.AsyncIterator; +} +declare module "node:fs/promises" { + export * from "fs/promises"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/globals.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/globals.d.ts new file mode 100644 index 00000000..83585974 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/globals.d.ts @@ -0,0 +1,172 @@ +declare var global: typeof globalThis; + +declare var process: NodeJS.Process; +declare var console: Console; + +interface ErrorConstructor { + /** + * Creates a `.stack` property on `targetObject`, which when accessed returns + * a string representing the location in the code at which + * `Error.captureStackTrace()` was called. + * + * ```js + * const myObject = {}; + * Error.captureStackTrace(myObject); + * myObject.stack; // Similar to `new Error().stack` + * ``` + * + * The first line of the trace will be prefixed with + * `${myObject.name}: ${myObject.message}`. + * + * The optional `constructorOpt` argument accepts a function. If given, all frames + * above `constructorOpt`, including `constructorOpt`, will be omitted from the + * generated stack trace. + * + * The `constructorOpt` argument is useful for hiding implementation + * details of error generation from the user. For instance: + * + * ```js + * function a() { + * b(); + * } + * + * function b() { + * c(); + * } + * + * function c() { + * // Create an error without stack trace to avoid calculating the stack trace twice. + * const { stackTraceLimit } = Error; + * Error.stackTraceLimit = 0; + * const error = new Error(); + * Error.stackTraceLimit = stackTraceLimit; + * + * // Capture the stack trace above function b + * Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + * throw error; + * } + * + * a(); + * ``` + */ + captureStackTrace(targetObject: object, constructorOpt?: Function): void; + /** + * @see https://v8.dev/docs/stack-trace-api#customizing-stack-traces + */ + prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any; + /** + * The `Error.stackTraceLimit` property specifies the number of stack frames + * collected by a stack trace (whether generated by `new Error().stack` or + * `Error.captureStackTrace(obj)`). + * + * The default value is `10` but may be set to any valid JavaScript number. Changes + * will affect any stack trace captured _after_ the value has been changed. + * + * If set to a non-number value, or set to a negative number, stack traces will + * not capture any frames. + */ + stackTraceLimit: number; +} + +/** + * Enable this API with the `--expose-gc` CLI flag. + */ +declare var gc: NodeJS.GCFunction | undefined; + +declare namespace NodeJS { + interface CallSite { + getColumnNumber(): number | null; + getEnclosingColumnNumber(): number | null; + getEnclosingLineNumber(): number | null; + getEvalOrigin(): string | undefined; + getFileName(): string | null; + getFunction(): Function | undefined; + getFunctionName(): string | null; + getLineNumber(): number | null; + getMethodName(): string | null; + getPosition(): number; + getPromiseIndex(): number | null; + getScriptHash(): string; + getScriptNameOrSourceURL(): string | null; + getThis(): unknown; + getTypeName(): string | null; + isAsync(): boolean; + isConstructor(): boolean; + isEval(): boolean; + isNative(): boolean; + isPromiseAll(): boolean; + isToplevel(): boolean; + } + + interface ErrnoException extends Error { + errno?: number | undefined; + code?: string | undefined; + path?: string | undefined; + syscall?: string | undefined; + } + + interface ReadableStream extends EventEmitter { + readable: boolean; + read(size?: number): string | Buffer; + setEncoding(encoding: BufferEncoding): this; + pause(): this; + resume(): this; + isPaused(): boolean; + pipe(destination: T, options?: { end?: boolean | undefined }): T; + unpipe(destination?: WritableStream): this; + unshift(chunk: string | Uint8Array, encoding?: BufferEncoding): void; + wrap(oldStream: ReadableStream): this; + [Symbol.asyncIterator](): AsyncIterableIterator; + } + + interface WritableStream extends EventEmitter { + writable: boolean; + write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean; + write(str: string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean; + end(cb?: () => void): this; + end(data: string | Uint8Array, cb?: () => void): this; + end(str: string, encoding?: BufferEncoding, cb?: () => void): this; + } + + interface ReadWriteStream extends ReadableStream, WritableStream {} + + interface RefCounted { + ref(): this; + unref(): this; + } + + interface Dict { + [key: string]: T | undefined; + } + + interface ReadOnlyDict { + readonly [key: string]: T | undefined; + } + + type PartialOptions = { [K in keyof T]?: T[K] | undefined }; + + interface GCFunction { + (minor?: boolean): void; + (options: NodeJS.GCOptions & { execution: "async" }): Promise; + (options: NodeJS.GCOptions): void; + } + + interface GCOptions { + execution?: "sync" | "async" | undefined; + flavor?: "regular" | "last-resort" | undefined; + type?: "major-snapshot" | "major" | "minor" | undefined; + filename?: string | undefined; + } + + /** An iterable iterator returned by the Node.js API. */ + // Default TReturn/TNext in v22 is `any`, for compatibility with the previously-used IterableIterator. + interface Iterator extends IteratorObject { + [Symbol.iterator](): NodeJS.Iterator; + } + + /** An async iterable iterator returned by the Node.js API. */ + // Default TReturn/TNext in v22 is `any`, for compatibility with the previously-used AsyncIterableIterator. + interface AsyncIterator extends AsyncIteratorObject { + [Symbol.asyncIterator](): NodeJS.AsyncIterator; + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/globals.typedarray.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/globals.typedarray.d.ts new file mode 100644 index 00000000..8eafc3b4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/globals.typedarray.d.ts @@ -0,0 +1,38 @@ +export {}; // Make this a module + +declare global { + namespace NodeJS { + type TypedArray = + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array + | Int8Array + | Int16Array + | Int32Array + | BigUint64Array + | BigInt64Array + | Float32Array + | Float64Array; + type ArrayBufferView = + | TypedArray + | DataView; + + // The following aliases are required to allow use of non-shared ArrayBufferViews in @types/node + // while maintaining compatibility with TS <=5.6. + type NonSharedUint8Array = Uint8Array; + type NonSharedUint8ClampedArray = Uint8ClampedArray; + type NonSharedUint16Array = Uint16Array; + type NonSharedUint32Array = Uint32Array; + type NonSharedInt8Array = Int8Array; + type NonSharedInt16Array = Int16Array; + type NonSharedInt32Array = Int32Array; + type NonSharedBigUint64Array = BigUint64Array; + type NonSharedBigInt64Array = BigInt64Array; + type NonSharedFloat32Array = Float32Array; + type NonSharedFloat64Array = Float64Array; + type NonSharedDataView = DataView; + type NonSharedTypedArray = TypedArray; + type NonSharedArrayBufferView = ArrayBufferView; + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/http.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/http.d.ts new file mode 100644 index 00000000..af7d21cd --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/http.d.ts @@ -0,0 +1,2089 @@ +/** + * To use the HTTP server and client one must import the `node:http` module. + * + * The HTTP interfaces in Node.js are designed to support many features + * of the protocol which have been traditionally difficult to use. + * In particular, large, possibly chunk-encoded, messages. The interface is + * careful to never buffer entire requests or responses, so the + * user is able to stream data. + * + * HTTP message headers are represented by an object like this: + * + * ```json + * { "content-length": "123", + * "content-type": "text/plain", + * "connection": "keep-alive", + * "host": "example.com", + * "accept": "*" } + * ``` + * + * Keys are lowercased. Values are not modified. + * + * In order to support the full spectrum of possible HTTP applications, the Node.js + * HTTP API is very low-level. It deals with stream handling and message + * parsing only. It parses a message into headers and body but it does not + * parse the actual headers or the body. + * + * See `message.headers` for details on how duplicate headers are handled. + * + * The raw headers as they were received are retained in the `rawHeaders` property, which is an array of `[key, value, key2, value2, ...]`. For + * example, the previous message header object might have a `rawHeaders` list like the following: + * + * ```js + * [ 'ConTent-Length', '123456', + * 'content-LENGTH', '123', + * 'content-type', 'text/plain', + * 'CONNECTION', 'keep-alive', + * 'Host', 'example.com', + * 'accepT', '*' ] + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/http.js) + */ +declare module "http" { + import { NonSharedBuffer } from "node:buffer"; + import * as stream from "node:stream"; + import { URL } from "node:url"; + import { LookupOptions } from "node:dns"; + import { EventEmitter } from "node:events"; + import { LookupFunction, Server as NetServer, Socket, TcpSocketConnectOpts } from "node:net"; + // incoming headers will never contain number + interface IncomingHttpHeaders extends NodeJS.Dict { + accept?: string | undefined; + "accept-encoding"?: string | undefined; + "accept-language"?: string | undefined; + "accept-patch"?: string | undefined; + "accept-ranges"?: string | undefined; + "access-control-allow-credentials"?: string | undefined; + "access-control-allow-headers"?: string | undefined; + "access-control-allow-methods"?: string | undefined; + "access-control-allow-origin"?: string | undefined; + "access-control-expose-headers"?: string | undefined; + "access-control-max-age"?: string | undefined; + "access-control-request-headers"?: string | undefined; + "access-control-request-method"?: string | undefined; + age?: string | undefined; + allow?: string | undefined; + "alt-svc"?: string | undefined; + authorization?: string | undefined; + "cache-control"?: string | undefined; + connection?: string | undefined; + "content-disposition"?: string | undefined; + "content-encoding"?: string | undefined; + "content-language"?: string | undefined; + "content-length"?: string | undefined; + "content-location"?: string | undefined; + "content-range"?: string | undefined; + "content-type"?: string | undefined; + cookie?: string | undefined; + date?: string | undefined; + etag?: string | undefined; + expect?: string | undefined; + expires?: string | undefined; + forwarded?: string | undefined; + from?: string | undefined; + host?: string | undefined; + "if-match"?: string | undefined; + "if-modified-since"?: string | undefined; + "if-none-match"?: string | undefined; + "if-unmodified-since"?: string | undefined; + "last-modified"?: string | undefined; + location?: string | undefined; + origin?: string | undefined; + pragma?: string | undefined; + "proxy-authenticate"?: string | undefined; + "proxy-authorization"?: string | undefined; + "public-key-pins"?: string | undefined; + range?: string | undefined; + referer?: string | undefined; + "retry-after"?: string | undefined; + "sec-fetch-site"?: string | undefined; + "sec-fetch-mode"?: string | undefined; + "sec-fetch-user"?: string | undefined; + "sec-fetch-dest"?: string | undefined; + "sec-websocket-accept"?: string | undefined; + "sec-websocket-extensions"?: string | undefined; + "sec-websocket-key"?: string | undefined; + "sec-websocket-protocol"?: string | undefined; + "sec-websocket-version"?: string | undefined; + "set-cookie"?: string[] | undefined; + "strict-transport-security"?: string | undefined; + tk?: string | undefined; + trailer?: string | undefined; + "transfer-encoding"?: string | undefined; + upgrade?: string | undefined; + "user-agent"?: string | undefined; + vary?: string | undefined; + via?: string | undefined; + warning?: string | undefined; + "www-authenticate"?: string | undefined; + } + // outgoing headers allows numbers (as they are converted internally to strings) + type OutgoingHttpHeader = number | string | string[]; + interface OutgoingHttpHeaders extends NodeJS.Dict { + accept?: string | string[] | undefined; + "accept-charset"?: string | string[] | undefined; + "accept-encoding"?: string | string[] | undefined; + "accept-language"?: string | string[] | undefined; + "accept-ranges"?: string | undefined; + "access-control-allow-credentials"?: string | undefined; + "access-control-allow-headers"?: string | undefined; + "access-control-allow-methods"?: string | undefined; + "access-control-allow-origin"?: string | undefined; + "access-control-expose-headers"?: string | undefined; + "access-control-max-age"?: string | undefined; + "access-control-request-headers"?: string | undefined; + "access-control-request-method"?: string | undefined; + age?: string | undefined; + allow?: string | undefined; + authorization?: string | undefined; + "cache-control"?: string | undefined; + "cdn-cache-control"?: string | undefined; + connection?: string | string[] | undefined; + "content-disposition"?: string | undefined; + "content-encoding"?: string | undefined; + "content-language"?: string | undefined; + "content-length"?: string | number | undefined; + "content-location"?: string | undefined; + "content-range"?: string | undefined; + "content-security-policy"?: string | undefined; + "content-security-policy-report-only"?: string | undefined; + "content-type"?: string | undefined; + cookie?: string | string[] | undefined; + dav?: string | string[] | undefined; + dnt?: string | undefined; + date?: string | undefined; + etag?: string | undefined; + expect?: string | undefined; + expires?: string | undefined; + forwarded?: string | undefined; + from?: string | undefined; + host?: string | undefined; + "if-match"?: string | undefined; + "if-modified-since"?: string | undefined; + "if-none-match"?: string | undefined; + "if-range"?: string | undefined; + "if-unmodified-since"?: string | undefined; + "last-modified"?: string | undefined; + link?: string | string[] | undefined; + location?: string | undefined; + "max-forwards"?: string | undefined; + origin?: string | undefined; + pragma?: string | string[] | undefined; + "proxy-authenticate"?: string | string[] | undefined; + "proxy-authorization"?: string | undefined; + "public-key-pins"?: string | undefined; + "public-key-pins-report-only"?: string | undefined; + range?: string | undefined; + referer?: string | undefined; + "referrer-policy"?: string | undefined; + refresh?: string | undefined; + "retry-after"?: string | undefined; + "sec-websocket-accept"?: string | undefined; + "sec-websocket-extensions"?: string | string[] | undefined; + "sec-websocket-key"?: string | undefined; + "sec-websocket-protocol"?: string | string[] | undefined; + "sec-websocket-version"?: string | undefined; + server?: string | undefined; + "set-cookie"?: string | string[] | undefined; + "strict-transport-security"?: string | undefined; + te?: string | undefined; + trailer?: string | undefined; + "transfer-encoding"?: string | undefined; + "user-agent"?: string | undefined; + upgrade?: string | undefined; + "upgrade-insecure-requests"?: string | undefined; + vary?: string | undefined; + via?: string | string[] | undefined; + warning?: string | undefined; + "www-authenticate"?: string | string[] | undefined; + "x-content-type-options"?: string | undefined; + "x-dns-prefetch-control"?: string | undefined; + "x-frame-options"?: string | undefined; + "x-xss-protection"?: string | undefined; + } + interface ClientRequestArgs extends Pick { + _defaultAgent?: Agent | undefined; + agent?: Agent | boolean | undefined; + auth?: string | null | undefined; + createConnection?: + | (( + options: ClientRequestArgs, + oncreate: (err: Error | null, socket: stream.Duplex) => void, + ) => stream.Duplex | null | undefined) + | undefined; + defaultPort?: number | string | undefined; + family?: number | undefined; + headers?: OutgoingHttpHeaders | readonly string[] | undefined; + host?: string | null | undefined; + hostname?: string | null | undefined; + insecureHTTPParser?: boolean | undefined; + localAddress?: string | undefined; + localPort?: number | undefined; + lookup?: LookupFunction | undefined; + /** + * @default 16384 + */ + maxHeaderSize?: number | undefined; + method?: string | undefined; + path?: string | null | undefined; + port?: number | string | null | undefined; + protocol?: string | null | undefined; + setDefaultHeaders?: boolean | undefined; + setHost?: boolean | undefined; + signal?: AbortSignal | undefined; + socketPath?: string | undefined; + timeout?: number | undefined; + uniqueHeaders?: Array | undefined; + joinDuplicateHeaders?: boolean | undefined; + } + interface ServerOptions< + Request extends typeof IncomingMessage = typeof IncomingMessage, + Response extends typeof ServerResponse> = typeof ServerResponse, + > { + /** + * Specifies the `IncomingMessage` class to be used. Useful for extending the original `IncomingMessage`. + */ + IncomingMessage?: Request | undefined; + /** + * Specifies the `ServerResponse` class to be used. Useful for extending the original `ServerResponse`. + */ + ServerResponse?: Response | undefined; + /** + * Sets the timeout value in milliseconds for receiving the entire request from the client. + * @see Server.requestTimeout for more information. + * @default 300000 + * @since v18.0.0 + */ + requestTimeout?: number | undefined; + /** + * It joins the field line values of multiple headers in a request with `, ` instead of discarding the duplicates. + * @default false + * @since v18.14.0 + */ + joinDuplicateHeaders?: boolean | undefined; + /** + * The number of milliseconds of inactivity a server needs to wait for additional incoming data, + * after it has finished writing the last response, before a socket will be destroyed. + * @see Server.keepAliveTimeout for more information. + * @default 5000 + * @since v18.0.0 + */ + keepAliveTimeout?: number | undefined; + /** + * An additional buffer time added to the + * `server.keepAliveTimeout` to extend the internal socket timeout. + * @since 22.19.0 + * @default 1000 + */ + keepAliveTimeoutBuffer?: number | undefined; + /** + * Sets the interval value in milliseconds to check for request and headers timeout in incomplete requests. + * @default 30000 + */ + connectionsCheckingInterval?: number | undefined; + /** + * Sets the timeout value in milliseconds for receiving the complete HTTP headers from the client. + * See {@link Server.headersTimeout} for more information. + * @default 60000 + * @since 18.0.0 + */ + headersTimeout?: number | undefined; + /** + * Optionally overrides all `socket`s' `readableHighWaterMark` and `writableHighWaterMark`. + * This affects `highWaterMark` property of both `IncomingMessage` and `ServerResponse`. + * Default: @see stream.getDefaultHighWaterMark(). + * @since v20.1.0 + */ + highWaterMark?: number | undefined; + /** + * Use an insecure HTTP parser that accepts invalid HTTP headers when `true`. + * Using the insecure parser should be avoided. + * See --insecure-http-parser for more information. + * @default false + */ + insecureHTTPParser?: boolean | undefined; + /** + * Optionally overrides the value of `--max-http-header-size` for requests received by + * this server, i.e. the maximum length of request headers in bytes. + * @default 16384 + * @since v13.3.0 + */ + maxHeaderSize?: number | undefined; + /** + * If set to `true`, it disables the use of Nagle's algorithm immediately after a new incoming connection is received. + * @default true + * @since v16.5.0 + */ + noDelay?: boolean | undefined; + /** + * If set to `true`, it forces the server to respond with a 400 (Bad Request) status code + * to any HTTP/1.1 request message that lacks a Host header (as mandated by the specification). + * @default true + * @since 20.0.0 + */ + requireHostHeader?: boolean | undefined; + /** + * If set to `true`, it enables keep-alive functionality on the socket immediately after a new incoming connection is received, + * similarly on what is done in `socket.setKeepAlive([enable][, initialDelay])`. + * @default false + * @since v16.5.0 + */ + keepAlive?: boolean | undefined; + /** + * If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket. + * @default 0 + * @since v16.5.0 + */ + keepAliveInitialDelay?: number | undefined; + /** + * A list of response headers that should be sent only once. + * If the header's value is an array, the items will be joined using `; `. + */ + uniqueHeaders?: Array | undefined; + /** + * If set to `true`, an error is thrown when writing to an HTTP response which does not have a body. + * @default false + * @since v18.17.0, v20.2.0 + */ + rejectNonStandardBodyWrites?: boolean | undefined; + } + type RequestListener< + Request extends typeof IncomingMessage = typeof IncomingMessage, + Response extends typeof ServerResponse> = typeof ServerResponse, + > = (req: InstanceType, res: InstanceType & { req: InstanceType }) => void; + /** + * @since v0.1.17 + */ + class Server< + Request extends typeof IncomingMessage = typeof IncomingMessage, + Response extends typeof ServerResponse> = typeof ServerResponse, + > extends NetServer { + constructor(requestListener?: RequestListener); + constructor(options: ServerOptions, requestListener?: RequestListener); + /** + * Sets the timeout value for sockets, and emits a `'timeout'` event on + * the Server object, passing the socket as an argument, if a timeout + * occurs. + * + * If there is a `'timeout'` event listener on the Server object, then it + * will be called with the timed-out socket as an argument. + * + * By default, the Server does not timeout sockets. However, if a callback + * is assigned to the Server's `'timeout'` event, timeouts must be handled + * explicitly. + * @since v0.9.12 + * @param [msecs=0 (no timeout)] + */ + setTimeout(msecs?: number, callback?: (socket: Socket) => void): this; + setTimeout(callback: (socket: Socket) => void): this; + /** + * Limits maximum incoming headers count. If set to 0, no limit will be applied. + * @since v0.7.0 + */ + maxHeadersCount: number | null; + /** + * The maximum number of requests socket can handle + * before closing keep alive connection. + * + * A value of `0` will disable the limit. + * + * When the limit is reached it will set the `Connection` header value to `close`, + * but will not actually close the connection, subsequent requests sent + * after the limit is reached will get `503 Service Unavailable` as a response. + * @since v16.10.0 + */ + maxRequestsPerSocket: number | null; + /** + * The number of milliseconds of inactivity before a socket is presumed + * to have timed out. + * + * A value of `0` will disable the timeout behavior on incoming connections. + * + * The socket timeout logic is set up on connection, so changing this + * value only affects new connections to the server, not any existing connections. + * @since v0.9.12 + */ + timeout: number; + /** + * Limit the amount of time the parser will wait to receive the complete HTTP + * headers. + * + * If the timeout expires, the server responds with status 408 without + * forwarding the request to the request listener and then closes the connection. + * + * It must be set to a non-zero value (e.g. 120 seconds) to protect against + * potential Denial-of-Service attacks in case the server is deployed without a + * reverse proxy in front. + * @since v11.3.0, v10.14.0 + */ + headersTimeout: number; + /** + * The number of milliseconds of inactivity a server needs to wait for additional + * incoming data, after it has finished writing the last response, before a socket + * will be destroyed. + * + * This timeout value is combined with the + * `server.keepAliveTimeoutBuffer` option to determine the actual socket + * timeout, calculated as: + * socketTimeout = keepAliveTimeout + keepAliveTimeoutBuffer + * If the server receives new data before the keep-alive timeout has fired, it + * will reset the regular inactivity timeout, i.e., `server.timeout`. + * + * A value of `0` will disable the keep-alive timeout behavior on incoming + * connections. + * A value of `0` makes the HTTP server behave similarly to Node.js versions prior + * to 8.0.0, which did not have a keep-alive timeout. + * + * The socket timeout logic is set up on connection, so changing this value only + * affects new connections to the server, not any existing connections. + * @since v8.0.0 + */ + keepAliveTimeout: number; + /** + * An additional buffer time added to the + * `server.keepAliveTimeout` to extend the internal socket timeout. + * + * This buffer helps reduce connection reset (`ECONNRESET`) errors by increasing + * the socket timeout slightly beyond the advertised keep-alive timeout. + * + * This option applies only to new incoming connections. + * @since v22.19.0 + * @default 1000 + */ + keepAliveTimeoutBuffer: number; + /** + * Sets the timeout value in milliseconds for receiving the entire request from + * the client. + * + * If the timeout expires, the server responds with status 408 without + * forwarding the request to the request listener and then closes the connection. + * + * It must be set to a non-zero value (e.g. 120 seconds) to protect against + * potential Denial-of-Service attacks in case the server is deployed without a + * reverse proxy in front. + * @since v14.11.0 + */ + requestTimeout: number; + /** + * Closes all connections connected to this server. + * @since v18.2.0 + */ + closeAllConnections(): void; + /** + * Closes all connections connected to this server which are not sending a request + * or waiting for a response. + * @since v18.2.0 + */ + closeIdleConnections(): void; + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "connection", listener: (socket: Socket) => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "listening", listener: () => void): this; + addListener(event: "checkContinue", listener: RequestListener): this; + addListener(event: "checkExpectation", listener: RequestListener): this; + addListener(event: "clientError", listener: (err: Error, socket: stream.Duplex) => void): this; + addListener( + event: "connect", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + addListener(event: "dropRequest", listener: (req: InstanceType, socket: stream.Duplex) => void): this; + addListener(event: "request", listener: RequestListener): this; + addListener( + event: "upgrade", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + emit(event: string, ...args: any[]): boolean; + emit(event: "close"): boolean; + emit(event: "connection", socket: Socket): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "listening"): boolean; + emit( + event: "checkContinue", + req: InstanceType, + res: InstanceType & { req: InstanceType }, + ): boolean; + emit( + event: "checkExpectation", + req: InstanceType, + res: InstanceType & { req: InstanceType }, + ): boolean; + emit(event: "clientError", err: Error, socket: stream.Duplex): boolean; + emit(event: "connect", req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer): boolean; + emit(event: "dropRequest", req: InstanceType, socket: stream.Duplex): boolean; + emit( + event: "request", + req: InstanceType, + res: InstanceType & { req: InstanceType }, + ): boolean; + emit(event: "upgrade", req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "close", listener: () => void): this; + on(event: "connection", listener: (socket: Socket) => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "listening", listener: () => void): this; + on(event: "checkContinue", listener: RequestListener): this; + on(event: "checkExpectation", listener: RequestListener): this; + on(event: "clientError", listener: (err: Error, socket: stream.Duplex) => void): this; + on( + event: "connect", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + on(event: "dropRequest", listener: (req: InstanceType, socket: stream.Duplex) => void): this; + on(event: "request", listener: RequestListener): this; + on( + event: "upgrade", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "connection", listener: (socket: Socket) => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "listening", listener: () => void): this; + once(event: "checkContinue", listener: RequestListener): this; + once(event: "checkExpectation", listener: RequestListener): this; + once(event: "clientError", listener: (err: Error, socket: stream.Duplex) => void): this; + once( + event: "connect", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + once(event: "dropRequest", listener: (req: InstanceType, socket: stream.Duplex) => void): this; + once(event: "request", listener: RequestListener): this; + once( + event: "upgrade", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "connection", listener: (socket: Socket) => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "listening", listener: () => void): this; + prependListener(event: "checkContinue", listener: RequestListener): this; + prependListener(event: "checkExpectation", listener: RequestListener): this; + prependListener(event: "clientError", listener: (err: Error, socket: stream.Duplex) => void): this; + prependListener( + event: "connect", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + prependListener( + event: "dropRequest", + listener: (req: InstanceType, socket: stream.Duplex) => void, + ): this; + prependListener(event: "request", listener: RequestListener): this; + prependListener( + event: "upgrade", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "connection", listener: (socket: Socket) => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "listening", listener: () => void): this; + prependOnceListener(event: "checkContinue", listener: RequestListener): this; + prependOnceListener(event: "checkExpectation", listener: RequestListener): this; + prependOnceListener(event: "clientError", listener: (err: Error, socket: stream.Duplex) => void): this; + prependOnceListener( + event: "connect", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + prependOnceListener( + event: "dropRequest", + listener: (req: InstanceType, socket: stream.Duplex) => void, + ): this; + prependOnceListener(event: "request", listener: RequestListener): this; + prependOnceListener( + event: "upgrade", + listener: (req: InstanceType, socket: stream.Duplex, head: NonSharedBuffer) => void, + ): this; + } + /** + * This class serves as the parent class of {@link ClientRequest} and {@link ServerResponse}. It is an abstract outgoing message from + * the perspective of the participants of an HTTP transaction. + * @since v0.1.17 + */ + class OutgoingMessage extends stream.Writable { + readonly req: Request; + chunkedEncoding: boolean; + shouldKeepAlive: boolean; + useChunkedEncodingByDefault: boolean; + sendDate: boolean; + /** + * @deprecated Use `writableEnded` instead. + */ + finished: boolean; + /** + * Read-only. `true` if the headers were sent, otherwise `false`. + * @since v0.9.3 + */ + readonly headersSent: boolean; + /** + * Alias of `outgoingMessage.socket`. + * @since v0.3.0 + * @deprecated Since v15.12.0,v14.17.1 - Use `socket` instead. + */ + readonly connection: Socket | null; + /** + * Reference to the underlying socket. Usually, users will not want to access + * this property. + * + * After calling `outgoingMessage.end()`, this property will be nulled. + * @since v0.3.0 + */ + readonly socket: Socket | null; + constructor(); + /** + * Once a socket is associated with the message and is connected, `socket.setTimeout()` will be called with `msecs` as the first parameter. + * @since v0.9.12 + * @param callback Optional function to be called when a timeout occurs. Same as binding to the `timeout` event. + */ + setTimeout(msecs: number, callback?: () => void): this; + /** + * Sets a single header value. If the header already exists in the to-be-sent + * headers, its value will be replaced. Use an array of strings to send multiple + * headers with the same name. + * @since v0.4.0 + * @param name Header name + * @param value Header value + */ + setHeader(name: string, value: number | string | readonly string[]): this; + /** + * Sets multiple header values for implicit headers. headers must be an instance of + * `Headers` or `Map`, if a header already exists in the to-be-sent headers, its + * value will be replaced. + * + * ```js + * const headers = new Headers({ foo: 'bar' }); + * outgoingMessage.setHeaders(headers); + * ``` + * + * or + * + * ```js + * const headers = new Map([['foo', 'bar']]); + * outgoingMessage.setHeaders(headers); + * ``` + * + * When headers have been set with `outgoingMessage.setHeaders()`, they will be + * merged with any headers passed to `response.writeHead()`, with the headers passed + * to `response.writeHead()` given precedence. + * + * ```js + * // Returns content-type = text/plain + * const server = http.createServer((req, res) => { + * const headers = new Headers({ 'Content-Type': 'text/html' }); + * res.setHeaders(headers); + * res.writeHead(200, { 'Content-Type': 'text/plain' }); + * res.end('ok'); + * }); + * ``` + * + * @since v19.6.0, v18.15.0 + * @param name Header name + * @param value Header value + */ + setHeaders(headers: Headers | Map): this; + /** + * Append a single header value to the header object. + * + * If the value is an array, this is equivalent to calling this method multiple + * times. + * + * If there were no previous values for the header, this is equivalent to calling `outgoingMessage.setHeader(name, value)`. + * + * Depending of the value of `options.uniqueHeaders` when the client request or the + * server were created, this will end up in the header being sent multiple times or + * a single time with values joined using `; `. + * @since v18.3.0, v16.17.0 + * @param name Header name + * @param value Header value + */ + appendHeader(name: string, value: string | readonly string[]): this; + /** + * Gets the value of the HTTP header with the given name. If that header is not + * set, the returned value will be `undefined`. + * @since v0.4.0 + * @param name Name of header + */ + getHeader(name: string): number | string | string[] | undefined; + /** + * Returns a shallow copy of the current outgoing headers. Since a shallow + * copy is used, array values may be mutated without additional calls to + * various header-related HTTP module methods. The keys of the returned + * object are the header names and the values are the respective header + * values. All header names are lowercase. + * + * The object returned by the `outgoingMessage.getHeaders()` method does + * not prototypically inherit from the JavaScript `Object`. This means that + * typical `Object` methods such as `obj.toString()`, `obj.hasOwnProperty()`, + * and others are not defined and will not work. + * + * ```js + * outgoingMessage.setHeader('Foo', 'bar'); + * outgoingMessage.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + * + * const headers = outgoingMessage.getHeaders(); + * // headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] } + * ``` + * @since v7.7.0 + */ + getHeaders(): OutgoingHttpHeaders; + /** + * Returns an array containing the unique names of the current outgoing headers. + * All names are lowercase. + * @since v7.7.0 + */ + getHeaderNames(): string[]; + /** + * Returns `true` if the header identified by `name` is currently set in the + * outgoing headers. The header name is case-insensitive. + * + * ```js + * const hasContentType = outgoingMessage.hasHeader('content-type'); + * ``` + * @since v7.7.0 + */ + hasHeader(name: string): boolean; + /** + * Removes a header that is queued for implicit sending. + * + * ```js + * outgoingMessage.removeHeader('Content-Encoding'); + * ``` + * @since v0.4.0 + * @param name Header name + */ + removeHeader(name: string): void; + /** + * Adds HTTP trailers (headers but at the end of the message) to the message. + * + * Trailers will **only** be emitted if the message is chunked encoded. If not, + * the trailers will be silently discarded. + * + * HTTP requires the `Trailer` header to be sent to emit trailers, + * with a list of header field names in its value, e.g. + * + * ```js + * message.writeHead(200, { 'Content-Type': 'text/plain', + * 'Trailer': 'Content-MD5' }); + * message.write(fileData); + * message.addTrailers({ 'Content-MD5': '7895bf4b8828b55ceaf47747b4bca667' }); + * message.end(); + * ``` + * + * Attempting to set a header field name or value that contains invalid characters + * will result in a `TypeError` being thrown. + * @since v0.3.0 + */ + addTrailers(headers: OutgoingHttpHeaders | ReadonlyArray<[string, string]>): void; + /** + * Flushes the message headers. + * + * For efficiency reason, Node.js normally buffers the message headers + * until `outgoingMessage.end()` is called or the first chunk of message data + * is written. It then tries to pack the headers and data into a single TCP + * packet. + * + * It is usually desired (it saves a TCP round-trip), but not when the first + * data is not sent until possibly much later. `outgoingMessage.flushHeaders()` bypasses the optimization and kickstarts the message. + * @since v1.6.0 + */ + flushHeaders(): void; + } + /** + * This object is created internally by an HTTP server, not by the user. It is + * passed as the second parameter to the `'request'` event. + * @since v0.1.17 + */ + class ServerResponse extends OutgoingMessage { + /** + * When using implicit headers (not calling `response.writeHead()` explicitly), + * this property controls the status code that will be sent to the client when + * the headers get flushed. + * + * ```js + * response.statusCode = 404; + * ``` + * + * After response header was sent to the client, this property indicates the + * status code which was sent out. + * @since v0.4.0 + */ + statusCode: number; + /** + * When using implicit headers (not calling `response.writeHead()` explicitly), + * this property controls the status message that will be sent to the client when + * the headers get flushed. If this is left as `undefined` then the standard + * message for the status code will be used. + * + * ```js + * response.statusMessage = 'Not found'; + * ``` + * + * After response header was sent to the client, this property indicates the + * status message which was sent out. + * @since v0.11.8 + */ + statusMessage: string; + /** + * If set to `true`, Node.js will check whether the `Content-Length` header value and the size of the body, in bytes, are equal. + * Mismatching the `Content-Length` header value will result + * in an `Error` being thrown, identified by `code:``'ERR_HTTP_CONTENT_LENGTH_MISMATCH'`. + * @since v18.10.0, v16.18.0 + */ + strictContentLength: boolean; + constructor(req: Request); + assignSocket(socket: Socket): void; + detachSocket(socket: Socket): void; + /** + * Sends an HTTP/1.1 100 Continue message to the client, indicating that + * the request body should be sent. See the `'checkContinue'` event on `Server`. + * @since v0.3.0 + */ + writeContinue(callback?: () => void): void; + /** + * Sends an HTTP/1.1 103 Early Hints message to the client with a Link header, + * indicating that the user agent can preload/preconnect the linked resources. + * The `hints` is an object containing the values of headers to be sent with + * early hints message. The optional `callback` argument will be called when + * the response message has been written. + * + * **Example** + * + * ```js + * const earlyHintsLink = '; rel=preload; as=style'; + * response.writeEarlyHints({ + * 'link': earlyHintsLink, + * }); + * + * const earlyHintsLinks = [ + * '; rel=preload; as=style', + * '; rel=preload; as=script', + * ]; + * response.writeEarlyHints({ + * 'link': earlyHintsLinks, + * 'x-trace-id': 'id for diagnostics', + * }); + * + * const earlyHintsCallback = () => console.log('early hints message sent'); + * response.writeEarlyHints({ + * 'link': earlyHintsLinks, + * }, earlyHintsCallback); + * ``` + * @since v18.11.0 + * @param hints An object containing the values of headers + * @param callback Will be called when the response message has been written + */ + writeEarlyHints(hints: Record, callback?: () => void): void; + /** + * Sends a response header to the request. The status code is a 3-digit HTTP + * status code, like `404`. The last argument, `headers`, are the response headers. + * Optionally one can give a human-readable `statusMessage` as the second + * argument. + * + * `headers` may be an `Array` where the keys and values are in the same list. + * It is _not_ a list of tuples. So, the even-numbered offsets are key values, + * and the odd-numbered offsets are the associated values. The array is in the same + * format as `request.rawHeaders`. + * + * Returns a reference to the `ServerResponse`, so that calls can be chained. + * + * ```js + * const body = 'hello world'; + * response + * .writeHead(200, { + * 'Content-Length': Buffer.byteLength(body), + * 'Content-Type': 'text/plain', + * }) + * .end(body); + * ``` + * + * This method must only be called once on a message and it must + * be called before `response.end()` is called. + * + * If `response.write()` or `response.end()` are called before calling + * this, the implicit/mutable headers will be calculated and call this function. + * + * When headers have been set with `response.setHeader()`, they will be merged + * with any headers passed to `response.writeHead()`, with the headers passed + * to `response.writeHead()` given precedence. + * + * If this method is called and `response.setHeader()` has not been called, + * it will directly write the supplied header values onto the network channel + * without caching internally, and the `response.getHeader()` on the header + * will not yield the expected result. If progressive population of headers is + * desired with potential future retrieval and modification, use `response.setHeader()` instead. + * + * ```js + * // Returns content-type = text/plain + * const server = http.createServer((req, res) => { + * res.setHeader('Content-Type', 'text/html'); + * res.setHeader('X-Foo', 'bar'); + * res.writeHead(200, { 'Content-Type': 'text/plain' }); + * res.end('ok'); + * }); + * ``` + * + * `Content-Length` is read in bytes, not characters. Use `Buffer.byteLength()` to determine the length of the body in bytes. Node.js + * will check whether `Content-Length` and the length of the body which has + * been transmitted are equal or not. + * + * Attempting to set a header field name or value that contains invalid characters + * will result in a `Error` being thrown. + * @since v0.1.30 + */ + writeHead( + statusCode: number, + statusMessage?: string, + headers?: OutgoingHttpHeaders | OutgoingHttpHeader[], + ): this; + writeHead(statusCode: number, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]): this; + /** + * Sends a HTTP/1.1 102 Processing message to the client, indicating that + * the request body should be sent. + * @since v10.0.0 + */ + writeProcessing(callback?: () => void): void; + } + interface InformationEvent { + statusCode: number; + statusMessage: string; + httpVersion: string; + httpVersionMajor: number; + httpVersionMinor: number; + headers: IncomingHttpHeaders; + rawHeaders: string[]; + } + /** + * This object is created internally and returned from {@link request}. It + * represents an _in-progress_ request whose header has already been queued. The + * header is still mutable using the `setHeader(name, value)`, `getHeader(name)`, `removeHeader(name)` API. The actual header will + * be sent along with the first data chunk or when calling `request.end()`. + * + * To get the response, add a listener for `'response'` to the request object. `'response'` will be emitted from the request object when the response + * headers have been received. The `'response'` event is executed with one + * argument which is an instance of {@link IncomingMessage}. + * + * During the `'response'` event, one can add listeners to the + * response object; particularly to listen for the `'data'` event. + * + * If no `'response'` handler is added, then the response will be + * entirely discarded. However, if a `'response'` event handler is added, + * then the data from the response object **must** be consumed, either by + * calling `response.read()` whenever there is a `'readable'` event, or + * by adding a `'data'` handler, or by calling the `.resume()` method. + * Until the data is consumed, the `'end'` event will not fire. Also, until + * the data is read it will consume memory that can eventually lead to a + * 'process out of memory' error. + * + * For backward compatibility, `res` will only emit `'error'` if there is an `'error'` listener registered. + * + * Set `Content-Length` header to limit the response body size. + * If `response.strictContentLength` is set to `true`, mismatching the `Content-Length` header value will result in an `Error` being thrown, + * identified by `code:``'ERR_HTTP_CONTENT_LENGTH_MISMATCH'`. + * + * `Content-Length` value should be in bytes, not characters. Use `Buffer.byteLength()` to determine the length of the body in bytes. + * @since v0.1.17 + */ + class ClientRequest extends OutgoingMessage { + /** + * The `request.aborted` property will be `true` if the request has + * been aborted. + * @since v0.11.14 + * @deprecated Since v17.0.0, v16.12.0 - Check `destroyed` instead. + */ + aborted: boolean; + /** + * The request host. + * @since v14.5.0, v12.19.0 + */ + host: string; + /** + * The request protocol. + * @since v14.5.0, v12.19.0 + */ + protocol: string; + /** + * When sending request through a keep-alive enabled agent, the underlying socket + * might be reused. But if server closes connection at unfortunate time, client + * may run into a 'ECONNRESET' error. + * + * ```js + * import http from 'node:http'; + * + * // Server has a 5 seconds keep-alive timeout by default + * http + * .createServer((req, res) => { + * res.write('hello\n'); + * res.end(); + * }) + * .listen(3000); + * + * setInterval(() => { + * // Adapting a keep-alive agent + * http.get('http://localhost:3000', { agent }, (res) => { + * res.on('data', (data) => { + * // Do nothing + * }); + * }); + * }, 5000); // Sending request on 5s interval so it's easy to hit idle timeout + * ``` + * + * By marking a request whether it reused socket or not, we can do + * automatic error retry base on it. + * + * ```js + * import http from 'node:http'; + * const agent = new http.Agent({ keepAlive: true }); + * + * function retriableRequest() { + * const req = http + * .get('http://localhost:3000', { agent }, (res) => { + * // ... + * }) + * .on('error', (err) => { + * // Check if retry is needed + * if (req.reusedSocket && err.code === 'ECONNRESET') { + * retriableRequest(); + * } + * }); + * } + * + * retriableRequest(); + * ``` + * @since v13.0.0, v12.16.0 + */ + reusedSocket: boolean; + /** + * Limits maximum response headers count. If set to 0, no limit will be applied. + */ + maxHeadersCount: number; + constructor(url: string | URL | ClientRequestArgs, cb?: (res: IncomingMessage) => void); + /** + * The request method. + * @since v0.1.97 + */ + method: string; + /** + * The request path. + * @since v0.4.0 + */ + path: string; + /** + * Marks the request as aborting. Calling this will cause remaining data + * in the response to be dropped and the socket to be destroyed. + * @since v0.3.8 + * @deprecated Since v14.1.0,v13.14.0 - Use `destroy` instead. + */ + abort(): void; + onSocket(socket: Socket): void; + /** + * Once a socket is assigned to this request and is connected `socket.setTimeout()` will be called. + * @since v0.5.9 + * @param timeout Milliseconds before a request times out. + * @param callback Optional function to be called when a timeout occurs. Same as binding to the `'timeout'` event. + */ + setTimeout(timeout: number, callback?: () => void): this; + /** + * Once a socket is assigned to this request and is connected `socket.setNoDelay()` will be called. + * @since v0.5.9 + */ + setNoDelay(noDelay?: boolean): void; + /** + * Once a socket is assigned to this request and is connected `socket.setKeepAlive()` will be called. + * @since v0.5.9 + */ + setSocketKeepAlive(enable?: boolean, initialDelay?: number): void; + /** + * Returns an array containing the unique names of the current outgoing raw + * headers. Header names are returned with their exact casing being set. + * + * ```js + * request.setHeader('Foo', 'bar'); + * request.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + * + * const headerNames = request.getRawHeaderNames(); + * // headerNames === ['Foo', 'Set-Cookie'] + * ``` + * @since v15.13.0, v14.17.0 + */ + getRawHeaderNames(): string[]; + /** + * @deprecated + */ + addListener(event: "abort", listener: () => void): this; + addListener( + event: "connect", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + addListener(event: "continue", listener: () => void): this; + addListener(event: "information", listener: (info: InformationEvent) => void): this; + addListener(event: "response", listener: (response: IncomingMessage) => void): this; + addListener(event: "socket", listener: (socket: Socket) => void): this; + addListener(event: "timeout", listener: () => void): this; + addListener( + event: "upgrade", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "finish", listener: () => void): this; + addListener(event: "pipe", listener: (src: stream.Readable) => void): this; + addListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + /** + * @deprecated + */ + on(event: "abort", listener: () => void): this; + on( + event: "connect", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + on(event: "continue", listener: () => void): this; + on(event: "information", listener: (info: InformationEvent) => void): this; + on(event: "response", listener: (response: IncomingMessage) => void): this; + on(event: "socket", listener: (socket: Socket) => void): this; + on(event: "timeout", listener: () => void): this; + on( + event: "upgrade", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + on(event: "close", listener: () => void): this; + on(event: "drain", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "finish", listener: () => void): this; + on(event: "pipe", listener: (src: stream.Readable) => void): this; + on(event: "unpipe", listener: (src: stream.Readable) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + /** + * @deprecated + */ + once(event: "abort", listener: () => void): this; + once( + event: "connect", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + once(event: "continue", listener: () => void): this; + once(event: "information", listener: (info: InformationEvent) => void): this; + once(event: "response", listener: (response: IncomingMessage) => void): this; + once(event: "socket", listener: (socket: Socket) => void): this; + once(event: "timeout", listener: () => void): this; + once( + event: "upgrade", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + once(event: "close", listener: () => void): this; + once(event: "drain", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "finish", listener: () => void): this; + once(event: "pipe", listener: (src: stream.Readable) => void): this; + once(event: "unpipe", listener: (src: stream.Readable) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + /** + * @deprecated + */ + prependListener(event: "abort", listener: () => void): this; + prependListener( + event: "connect", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + prependListener(event: "continue", listener: () => void): this; + prependListener(event: "information", listener: (info: InformationEvent) => void): this; + prependListener(event: "response", listener: (response: IncomingMessage) => void): this; + prependListener(event: "socket", listener: (socket: Socket) => void): this; + prependListener(event: "timeout", listener: () => void): this; + prependListener( + event: "upgrade", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "finish", listener: () => void): this; + prependListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + /** + * @deprecated + */ + prependOnceListener(event: "abort", listener: () => void): this; + prependOnceListener( + event: "connect", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + prependOnceListener(event: "continue", listener: () => void): this; + prependOnceListener(event: "information", listener: (info: InformationEvent) => void): this; + prependOnceListener(event: "response", listener: (response: IncomingMessage) => void): this; + prependOnceListener(event: "socket", listener: (socket: Socket) => void): this; + prependOnceListener(event: "timeout", listener: () => void): this; + prependOnceListener( + event: "upgrade", + listener: (response: IncomingMessage, socket: Socket, head: NonSharedBuffer) => void, + ): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "finish", listener: () => void): this; + prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + /** + * An `IncomingMessage` object is created by {@link Server} or {@link ClientRequest} and passed as the first argument to the `'request'` and `'response'` event respectively. It may be used to + * access response + * status, headers, and data. + * + * Different from its `socket` value which is a subclass of `stream.Duplex`, the `IncomingMessage` itself extends `stream.Readable` and is created separately to + * parse and emit the incoming HTTP headers and payload, as the underlying socket + * may be reused multiple times in case of keep-alive. + * @since v0.1.17 + */ + class IncomingMessage extends stream.Readable { + constructor(socket: Socket); + /** + * The `message.aborted` property will be `true` if the request has + * been aborted. + * @since v10.1.0 + * @deprecated Since v17.0.0,v16.12.0 - Check `message.destroyed` from stream.Readable. + */ + aborted: boolean; + /** + * In case of server request, the HTTP version sent by the client. In the case of + * client response, the HTTP version of the connected-to server. + * Probably either `'1.1'` or `'1.0'`. + * + * Also `message.httpVersionMajor` is the first integer and `message.httpVersionMinor` is the second. + * @since v0.1.1 + */ + httpVersion: string; + httpVersionMajor: number; + httpVersionMinor: number; + /** + * The `message.complete` property will be `true` if a complete HTTP message has + * been received and successfully parsed. + * + * This property is particularly useful as a means of determining if a client or + * server fully transmitted a message before a connection was terminated: + * + * ```js + * const req = http.request({ + * host: '127.0.0.1', + * port: 8080, + * method: 'POST', + * }, (res) => { + * res.resume(); + * res.on('end', () => { + * if (!res.complete) + * console.error( + * 'The connection was terminated while the message was still being sent'); + * }); + * }); + * ``` + * @since v0.3.0 + */ + complete: boolean; + /** + * Alias for `message.socket`. + * @since v0.1.90 + * @deprecated Since v16.0.0 - Use `socket`. + */ + connection: Socket; + /** + * The `net.Socket` object associated with the connection. + * + * With HTTPS support, use `request.socket.getPeerCertificate()` to obtain the + * client's authentication details. + * + * This property is guaranteed to be an instance of the `net.Socket` class, + * a subclass of `stream.Duplex`, unless the user specified a socket + * type other than `net.Socket` or internally nulled. + * @since v0.3.0 + */ + socket: Socket; + /** + * The request/response headers object. + * + * Key-value pairs of header names and values. Header names are lower-cased. + * + * ```js + * // Prints something like: + * // + * // { 'user-agent': 'curl/7.22.0', + * // host: '127.0.0.1:8000', + * // accept: '*' } + * console.log(request.headers); + * ``` + * + * Duplicates in raw headers are handled in the following ways, depending on the + * header name: + * + * * Duplicates of `age`, `authorization`, `content-length`, `content-type`, `etag`, `expires`, `from`, `host`, `if-modified-since`, `if-unmodified-since`, `last-modified`, `location`, + * `max-forwards`, `proxy-authorization`, `referer`, `retry-after`, `server`, or `user-agent` are discarded. + * To allow duplicate values of the headers listed above to be joined, + * use the option `joinDuplicateHeaders` in {@link request} and {@link createServer}. See RFC 9110 Section 5.3 for more + * information. + * * `set-cookie` is always an array. Duplicates are added to the array. + * * For duplicate `cookie` headers, the values are joined together with `; `. + * * For all other headers, the values are joined together with `, `. + * @since v0.1.5 + */ + headers: IncomingHttpHeaders; + /** + * Similar to `message.headers`, but there is no join logic and the values are + * always arrays of strings, even for headers received just once. + * + * ```js + * // Prints something like: + * // + * // { 'user-agent': ['curl/7.22.0'], + * // host: ['127.0.0.1:8000'], + * // accept: ['*'] } + * console.log(request.headersDistinct); + * ``` + * @since v18.3.0, v16.17.0 + */ + headersDistinct: NodeJS.Dict; + /** + * The raw request/response headers list exactly as they were received. + * + * The keys and values are in the same list. It is _not_ a + * list of tuples. So, the even-numbered offsets are key values, and the + * odd-numbered offsets are the associated values. + * + * Header names are not lowercased, and duplicates are not merged. + * + * ```js + * // Prints something like: + * // + * // [ 'user-agent', + * // 'this is invalid because there can be only one', + * // 'User-Agent', + * // 'curl/7.22.0', + * // 'Host', + * // '127.0.0.1:8000', + * // 'ACCEPT', + * // '*' ] + * console.log(request.rawHeaders); + * ``` + * @since v0.11.6 + */ + rawHeaders: string[]; + /** + * The request/response trailers object. Only populated at the `'end'` event. + * @since v0.3.0 + */ + trailers: NodeJS.Dict; + /** + * Similar to `message.trailers`, but there is no join logic and the values are + * always arrays of strings, even for headers received just once. + * Only populated at the `'end'` event. + * @since v18.3.0, v16.17.0 + */ + trailersDistinct: NodeJS.Dict; + /** + * The raw request/response trailer keys and values exactly as they were + * received. Only populated at the `'end'` event. + * @since v0.11.6 + */ + rawTrailers: string[]; + /** + * Calls `message.socket.setTimeout(msecs, callback)`. + * @since v0.5.9 + */ + setTimeout(msecs: number, callback?: () => void): this; + /** + * **Only valid for request obtained from {@link Server}.** + * + * The request method as a string. Read only. Examples: `'GET'`, `'DELETE'`. + * @since v0.1.1 + */ + method?: string | undefined; + /** + * **Only valid for request obtained from {@link Server}.** + * + * Request URL string. This contains only the URL that is present in the actual + * HTTP request. Take the following request: + * + * ```http + * GET /status?name=ryan HTTP/1.1 + * Accept: text/plain + * ``` + * + * To parse the URL into its parts: + * + * ```js + * new URL(`http://${process.env.HOST ?? 'localhost'}${request.url}`); + * ``` + * + * When `request.url` is `'/status?name=ryan'` and `process.env.HOST` is undefined: + * + * ```console + * $ node + * > new URL(`http://${process.env.HOST ?? 'localhost'}${request.url}`); + * URL { + * href: 'http://localhost/status?name=ryan', + * origin: 'http://localhost', + * protocol: 'http:', + * username: '', + * password: '', + * host: 'localhost', + * hostname: 'localhost', + * port: '', + * pathname: '/status', + * search: '?name=ryan', + * searchParams: URLSearchParams { 'name' => 'ryan' }, + * hash: '' + * } + * ``` + * + * Ensure that you set `process.env.HOST` to the server's host name, or consider replacing this part entirely. If using `req.headers.host`, ensure proper + * validation is used, as clients may specify a custom `Host` header. + * @since v0.1.90 + */ + url?: string | undefined; + /** + * **Only valid for response obtained from {@link ClientRequest}.** + * + * The 3-digit HTTP response status code. E.G. `404`. + * @since v0.1.1 + */ + statusCode?: number | undefined; + /** + * **Only valid for response obtained from {@link ClientRequest}.** + * + * The HTTP response status message (reason phrase). E.G. `OK` or `Internal Server Error`. + * @since v0.11.10 + */ + statusMessage?: string | undefined; + /** + * Calls `destroy()` on the socket that received the `IncomingMessage`. If `error` is provided, an `'error'` event is emitted on the socket and `error` is passed + * as an argument to any listeners on the event. + * @since v0.3.0 + */ + destroy(error?: Error): this; + } + interface AgentOptions extends NodeJS.PartialOptions { + /** + * Keep sockets around in a pool to be used by other requests in the future. Default = false + */ + keepAlive?: boolean | undefined; + /** + * When using HTTP KeepAlive, how often to send TCP KeepAlive packets over sockets being kept alive. Default = 1000. + * Only relevant if keepAlive is set to true. + */ + keepAliveMsecs?: number | undefined; + /** + * Maximum number of sockets to allow per host. Default for Node 0.10 is 5, default for Node 0.12 is Infinity + */ + maxSockets?: number | undefined; + /** + * Maximum number of sockets allowed for all hosts in total. Each request will use a new socket until the maximum is reached. Default: Infinity. + */ + maxTotalSockets?: number | undefined; + /** + * Maximum number of sockets to leave open in a free state. Only relevant if keepAlive is set to true. Default = 256. + */ + maxFreeSockets?: number | undefined; + /** + * Socket timeout in milliseconds. This will set the timeout after the socket is connected. + */ + timeout?: number | undefined; + /** + * Scheduling strategy to apply when picking the next free socket to use. + * @default `lifo` + */ + scheduling?: "fifo" | "lifo" | undefined; + } + /** + * An `Agent` is responsible for managing connection persistence + * and reuse for HTTP clients. It maintains a queue of pending requests + * for a given host and port, reusing a single socket connection for each + * until the queue is empty, at which time the socket is either destroyed + * or put into a pool where it is kept to be used again for requests to the + * same host and port. Whether it is destroyed or pooled depends on the `keepAlive` `option`. + * + * Pooled connections have TCP Keep-Alive enabled for them, but servers may + * still close idle connections, in which case they will be removed from the + * pool and a new connection will be made when a new HTTP request is made for + * that host and port. Servers may also refuse to allow multiple requests + * over the same connection, in which case the connection will have to be + * remade for every request and cannot be pooled. The `Agent` will still make + * the requests to that server, but each one will occur over a new connection. + * + * When a connection is closed by the client or the server, it is removed + * from the pool. Any unused sockets in the pool will be unrefed so as not + * to keep the Node.js process running when there are no outstanding requests. + * (see `socket.unref()`). + * + * It is good practice, to `destroy()` an `Agent` instance when it is no + * longer in use, because unused sockets consume OS resources. + * + * Sockets are removed from an agent when the socket emits either + * a `'close'` event or an `'agentRemove'` event. When intending to keep one + * HTTP request open for a long time without keeping it in the agent, something + * like the following may be done: + * + * ```js + * http.get(options, (res) => { + * // Do stuff + * }).on('socket', (socket) => { + * socket.emit('agentRemove'); + * }); + * ``` + * + * An agent may also be used for an individual request. By providing `{agent: false}` as an option to the `http.get()` or `http.request()` functions, a one-time use `Agent` with default options + * will be used + * for the client connection. + * + * `agent:false`: + * + * ```js + * http.get({ + * hostname: 'localhost', + * port: 80, + * path: '/', + * agent: false, // Create a new agent just for this one request + * }, (res) => { + * // Do stuff with response + * }); + * ``` + * + * `options` in [`socket.connect()`](https://nodejs.org/docs/latest-v22.x/api/net.html#socketconnectoptions-connectlistener) are also supported. + * + * To configure any of them, a custom {@link Agent} instance must be created. + * + * ```js + * import http from 'node:http'; + * const keepAliveAgent = new http.Agent({ keepAlive: true }); + * options.agent = keepAliveAgent; + * http.request(options, onResponseCallback) + * ``` + * @since v0.3.4 + */ + class Agent extends EventEmitter { + /** + * By default set to 256. For agents with `keepAlive` enabled, this + * sets the maximum number of sockets that will be left open in the free + * state. + * @since v0.11.7 + */ + maxFreeSockets: number; + /** + * By default set to `Infinity`. Determines how many concurrent sockets the agent + * can have open per origin. Origin is the returned value of `agent.getName()`. + * @since v0.3.6 + */ + maxSockets: number; + /** + * By default set to `Infinity`. Determines how many concurrent sockets the agent + * can have open. Unlike `maxSockets`, this parameter applies across all origins. + * @since v14.5.0, v12.19.0 + */ + maxTotalSockets: number; + /** + * An object which contains arrays of sockets currently awaiting use by + * the agent when `keepAlive` is enabled. Do not modify. + * + * Sockets in the `freeSockets` list will be automatically destroyed and + * removed from the array on `'timeout'`. + * @since v0.11.4 + */ + readonly freeSockets: NodeJS.ReadOnlyDict; + /** + * An object which contains arrays of sockets currently in use by the + * agent. Do not modify. + * @since v0.3.6 + */ + readonly sockets: NodeJS.ReadOnlyDict; + /** + * An object which contains queues of requests that have not yet been assigned to + * sockets. Do not modify. + * @since v0.5.9 + */ + readonly requests: NodeJS.ReadOnlyDict; + constructor(opts?: AgentOptions); + /** + * Destroy any sockets that are currently in use by the agent. + * + * It is usually not necessary to do this. However, if using an + * agent with `keepAlive` enabled, then it is best to explicitly shut down + * the agent when it is no longer needed. Otherwise, + * sockets might stay open for quite a long time before the server + * terminates them. + * @since v0.11.4 + */ + destroy(): void; + /** + * Produces a socket/stream to be used for HTTP requests. + * + * By default, this function is the same as `net.createConnection()`. However, + * custom agents may override this method in case greater flexibility is desired. + * + * A socket/stream can be supplied in one of two ways: by returning the + * socket/stream from this function, or by passing the socket/stream to `callback`. + * + * This method is guaranteed to return an instance of the `net.Socket` class, + * a subclass of `stream.Duplex`, unless the user specifies a socket + * type other than `net.Socket`. + * + * `callback` has a signature of `(err, stream)`. + * @since v0.11.4 + * @param options Options containing connection details. Check `createConnection` for the format of the options + * @param callback Callback function that receives the created socket + */ + createConnection( + options: ClientRequestArgs, + callback?: (err: Error | null, stream: stream.Duplex) => void, + ): stream.Duplex | null | undefined; + /** + * Called when `socket` is detached from a request and could be persisted by the`Agent`. Default behavior is to: + * + * ```js + * socket.setKeepAlive(true, this.keepAliveMsecs); + * socket.unref(); + * return true; + * ``` + * + * This method can be overridden by a particular `Agent` subclass. If this + * method returns a falsy value, the socket will be destroyed instead of persisting + * it for use with the next request. + * + * The `socket` argument can be an instance of `net.Socket`, a subclass of `stream.Duplex`. + * @since v8.1.0 + */ + keepSocketAlive(socket: stream.Duplex): void; + /** + * Called when `socket` is attached to `request` after being persisted because of + * the keep-alive options. Default behavior is to: + * + * ```js + * socket.ref(); + * ``` + * + * This method can be overridden by a particular `Agent` subclass. + * + * The `socket` argument can be an instance of `net.Socket`, a subclass of `stream.Duplex`. + * @since v8.1.0 + */ + reuseSocket(socket: stream.Duplex, request: ClientRequest): void; + /** + * Get a unique name for a set of request options, to determine whether a + * connection can be reused. For an HTTP agent, this returns`host:port:localAddress` or `host:port:localAddress:family`. For an HTTPS agent, + * the name includes the CA, cert, ciphers, and other HTTPS/TLS-specific options + * that determine socket reusability. + * @since v0.11.4 + * @param options A set of options providing information for name generation + */ + getName(options?: ClientRequestArgs): string; + } + const METHODS: string[]; + const STATUS_CODES: { + [errorCode: number]: string | undefined; + [errorCode: string]: string | undefined; + }; + /** + * Returns a new instance of {@link Server}. + * + * The `requestListener` is a function which is automatically + * added to the `'request'` event. + * + * ```js + * import http from 'node:http'; + * + * // Create a local server to receive data from + * const server = http.createServer((req, res) => { + * res.writeHead(200, { 'Content-Type': 'application/json' }); + * res.end(JSON.stringify({ + * data: 'Hello World!', + * })); + * }); + * + * server.listen(8000); + * ``` + * + * ```js + * import http from 'node:http'; + * + * // Create a local server to receive data from + * const server = http.createServer(); + * + * // Listen to the request event + * server.on('request', (request, res) => { + * res.writeHead(200, { 'Content-Type': 'application/json' }); + * res.end(JSON.stringify({ + * data: 'Hello World!', + * })); + * }); + * + * server.listen(8000); + * ``` + * @since v0.1.13 + */ + function createServer< + Request extends typeof IncomingMessage = typeof IncomingMessage, + Response extends typeof ServerResponse> = typeof ServerResponse, + >(requestListener?: RequestListener): Server; + function createServer< + Request extends typeof IncomingMessage = typeof IncomingMessage, + Response extends typeof ServerResponse> = typeof ServerResponse, + >( + options: ServerOptions, + requestListener?: RequestListener, + ): Server; + // although RequestOptions are passed as ClientRequestArgs to ClientRequest directly, + // create interface RequestOptions would make the naming more clear to developers + interface RequestOptions extends ClientRequestArgs {} + /** + * `options` in `socket.connect()` are also supported. + * + * Node.js maintains several connections per server to make HTTP requests. + * This function allows one to transparently issue requests. + * + * `url` can be a string or a `URL` object. If `url` is a + * string, it is automatically parsed with `new URL()`. If it is a `URL` object, it will be automatically converted to an ordinary `options` object. + * + * If both `url` and `options` are specified, the objects are merged, with the `options` properties taking precedence. + * + * The optional `callback` parameter will be added as a one-time listener for + * the `'response'` event. + * + * `http.request()` returns an instance of the {@link ClientRequest} class. The `ClientRequest` instance is a writable stream. If one needs to + * upload a file with a POST request, then write to the `ClientRequest` object. + * + * ```js + * import http from 'node:http'; + * import { Buffer } from 'node:buffer'; + * + * const postData = JSON.stringify({ + * 'msg': 'Hello World!', + * }); + * + * const options = { + * hostname: 'www.google.com', + * port: 80, + * path: '/upload', + * method: 'POST', + * headers: { + * 'Content-Type': 'application/json', + * 'Content-Length': Buffer.byteLength(postData), + * }, + * }; + * + * const req = http.request(options, (res) => { + * console.log(`STATUS: ${res.statusCode}`); + * console.log(`HEADERS: ${JSON.stringify(res.headers)}`); + * res.setEncoding('utf8'); + * res.on('data', (chunk) => { + * console.log(`BODY: ${chunk}`); + * }); + * res.on('end', () => { + * console.log('No more data in response.'); + * }); + * }); + * + * req.on('error', (e) => { + * console.error(`problem with request: ${e.message}`); + * }); + * + * // Write data to request body + * req.write(postData); + * req.end(); + * ``` + * + * In the example `req.end()` was called. With `http.request()` one + * must always call `req.end()` to signify the end of the request - + * even if there is no data being written to the request body. + * + * If any error is encountered during the request (be that with DNS resolution, + * TCP level errors, or actual HTTP parse errors) an `'error'` event is emitted + * on the returned request object. As with all `'error'` events, if no listeners + * are registered the error will be thrown. + * + * There are a few special headers that should be noted. + * + * * Sending a 'Connection: keep-alive' will notify Node.js that the connection to + * the server should be persisted until the next request. + * * Sending a 'Content-Length' header will disable the default chunked encoding. + * * Sending an 'Expect' header will immediately send the request headers. + * Usually, when sending 'Expect: 100-continue', both a timeout and a listener + * for the `'continue'` event should be set. See RFC 2616 Section 8.2.3 for more + * information. + * * Sending an Authorization header will override using the `auth` option + * to compute basic authentication. + * + * Example using a `URL` as `options`: + * + * ```js + * const options = new URL('http://abc:xyz@example.com'); + * + * const req = http.request(options, (res) => { + * // ... + * }); + * ``` + * + * In a successful request, the following events will be emitted in the following + * order: + * + * * `'socket'` + * * `'response'` + * * `'data'` any number of times, on the `res` object + * (`'data'` will not be emitted at all if the response body is empty, for + * instance, in most redirects) + * * `'end'` on the `res` object + * * `'close'` + * + * In the case of a connection error, the following events will be emitted: + * + * * `'socket'` + * * `'error'` + * * `'close'` + * + * In the case of a premature connection close before the response is received, + * the following events will be emitted in the following order: + * + * * `'socket'` + * * `'error'` with an error with message `'Error: socket hang up'` and code `'ECONNRESET'` + * * `'close'` + * + * In the case of a premature connection close after the response is received, + * the following events will be emitted in the following order: + * + * * `'socket'` + * * `'response'` + * * `'data'` any number of times, on the `res` object + * * (connection closed here) + * * `'aborted'` on the `res` object + * * `'close'` + * * `'error'` on the `res` object with an error with message `'Error: aborted'` and code `'ECONNRESET'` + * * `'close'` on the `res` object + * + * If `req.destroy()` is called before a socket is assigned, the following + * events will be emitted in the following order: + * + * * (`req.destroy()` called here) + * * `'error'` with an error with message `'Error: socket hang up'` and code `'ECONNRESET'`, or the error with which `req.destroy()` was called + * * `'close'` + * + * If `req.destroy()` is called before the connection succeeds, the following + * events will be emitted in the following order: + * + * * `'socket'` + * * (`req.destroy()` called here) + * * `'error'` with an error with message `'Error: socket hang up'` and code `'ECONNRESET'`, or the error with which `req.destroy()` was called + * * `'close'` + * + * If `req.destroy()` is called after the response is received, the following + * events will be emitted in the following order: + * + * * `'socket'` + * * `'response'` + * * `'data'` any number of times, on the `res` object + * * (`req.destroy()` called here) + * * `'aborted'` on the `res` object + * * `'close'` + * * `'error'` on the `res` object with an error with message `'Error: aborted'` and code `'ECONNRESET'`, or the error with which `req.destroy()` was called + * * `'close'` on the `res` object + * + * If `req.abort()` is called before a socket is assigned, the following + * events will be emitted in the following order: + * + * * (`req.abort()` called here) + * * `'abort'` + * * `'close'` + * + * If `req.abort()` is called before the connection succeeds, the following + * events will be emitted in the following order: + * + * * `'socket'` + * * (`req.abort()` called here) + * * `'abort'` + * * `'error'` with an error with message `'Error: socket hang up'` and code `'ECONNRESET'` + * * `'close'` + * + * If `req.abort()` is called after the response is received, the following + * events will be emitted in the following order: + * + * * `'socket'` + * * `'response'` + * * `'data'` any number of times, on the `res` object + * * (`req.abort()` called here) + * * `'abort'` + * * `'aborted'` on the `res` object + * * `'error'` on the `res` object with an error with message `'Error: aborted'` and code `'ECONNRESET'`. + * * `'close'` + * * `'close'` on the `res` object + * + * Setting the `timeout` option or using the `setTimeout()` function will + * not abort the request or do anything besides add a `'timeout'` event. + * + * Passing an `AbortSignal` and then calling `abort()` on the corresponding `AbortController` will behave the same way as calling `.destroy()` on the + * request. Specifically, the `'error'` event will be emitted with an error with + * the message `'AbortError: The operation was aborted'`, the code `'ABORT_ERR'` and the `cause`, if one was provided. + * @since v0.3.6 + */ + function request(options: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void): ClientRequest; + function request( + url: string | URL, + options: RequestOptions, + callback?: (res: IncomingMessage) => void, + ): ClientRequest; + /** + * Since most requests are GET requests without bodies, Node.js provides this + * convenience method. The only difference between this method and {@link request} is that it sets the method to GET by default and calls `req.end()` automatically. The callback must take care to + * consume the response + * data for reasons stated in {@link ClientRequest} section. + * + * The `callback` is invoked with a single argument that is an instance of {@link IncomingMessage}. + * + * JSON fetching example: + * + * ```js + * http.get('http://localhost:8000/', (res) => { + * const { statusCode } = res; + * const contentType = res.headers['content-type']; + * + * let error; + * // Any 2xx status code signals a successful response but + * // here we're only checking for 200. + * if (statusCode !== 200) { + * error = new Error('Request Failed.\n' + + * `Status Code: ${statusCode}`); + * } else if (!/^application\/json/.test(contentType)) { + * error = new Error('Invalid content-type.\n' + + * `Expected application/json but received ${contentType}`); + * } + * if (error) { + * console.error(error.message); + * // Consume response data to free up memory + * res.resume(); + * return; + * } + * + * res.setEncoding('utf8'); + * let rawData = ''; + * res.on('data', (chunk) => { rawData += chunk; }); + * res.on('end', () => { + * try { + * const parsedData = JSON.parse(rawData); + * console.log(parsedData); + * } catch (e) { + * console.error(e.message); + * } + * }); + * }).on('error', (e) => { + * console.error(`Got error: ${e.message}`); + * }); + * + * // Create a local server to receive data from + * const server = http.createServer((req, res) => { + * res.writeHead(200, { 'Content-Type': 'application/json' }); + * res.end(JSON.stringify({ + * data: 'Hello World!', + * })); + * }); + * + * server.listen(8000); + * ``` + * @since v0.3.6 + * @param options Accepts the same `options` as {@link request}, with the method set to GET by default. + */ + function get(options: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void): ClientRequest; + function get(url: string | URL, options: RequestOptions, callback?: (res: IncomingMessage) => void): ClientRequest; + /** + * Performs the low-level validations on the provided `name` that are done when `res.setHeader(name, value)` is called. + * + * Passing illegal value as `name` will result in a `TypeError` being thrown, + * identified by `code: 'ERR_INVALID_HTTP_TOKEN'`. + * + * It is not necessary to use this method before passing headers to an HTTP request + * or response. The HTTP module will automatically validate such headers. + * + * Example: + * + * ```js + * import { validateHeaderName } from 'node:http'; + * + * try { + * validateHeaderName(''); + * } catch (err) { + * console.error(err instanceof TypeError); // --> true + * console.error(err.code); // --> 'ERR_INVALID_HTTP_TOKEN' + * console.error(err.message); // --> 'Header name must be a valid HTTP token [""]' + * } + * ``` + * @since v14.3.0 + * @param [label='Header name'] Label for error message. + */ + function validateHeaderName(name: string): void; + /** + * Performs the low-level validations on the provided `value` that are done when `res.setHeader(name, value)` is called. + * + * Passing illegal value as `value` will result in a `TypeError` being thrown. + * + * * Undefined value error is identified by `code: 'ERR_HTTP_INVALID_HEADER_VALUE'`. + * * Invalid value character error is identified by `code: 'ERR_INVALID_CHAR'`. + * + * It is not necessary to use this method before passing headers to an HTTP request + * or response. The HTTP module will automatically validate such headers. + * + * Examples: + * + * ```js + * import { validateHeaderValue } from 'node:http'; + * + * try { + * validateHeaderValue('x-my-header', undefined); + * } catch (err) { + * console.error(err instanceof TypeError); // --> true + * console.error(err.code === 'ERR_HTTP_INVALID_HEADER_VALUE'); // --> true + * console.error(err.message); // --> 'Invalid value "undefined" for header "x-my-header"' + * } + * + * try { + * validateHeaderValue('x-my-header', 'oʊmɪɡə'); + * } catch (err) { + * console.error(err instanceof TypeError); // --> true + * console.error(err.code === 'ERR_INVALID_CHAR'); // --> true + * console.error(err.message); // --> 'Invalid character in header content ["x-my-header"]' + * } + * ``` + * @since v14.3.0 + * @param name Header name + * @param value Header value + */ + function validateHeaderValue(name: string, value: string): void; + /** + * Set the maximum number of idle HTTP parsers. + * @since v18.8.0, v16.18.0 + * @param [max=1000] + */ + function setMaxIdleHTTPParsers(max: number): void; + /** + * Global instance of `Agent` which is used as the default for all HTTP client + * requests. Diverges from a default `Agent` configuration by having `keepAlive` + * enabled and a `timeout` of 5 seconds. + * @since v0.5.9 + */ + let globalAgent: Agent; + /** + * Read-only property specifying the maximum allowed size of HTTP headers in bytes. + * Defaults to 16KB. Configurable using the `--max-http-header-size` CLI option. + */ + const maxHeaderSize: number; + /** + * A browser-compatible implementation of `WebSocket`. + * @since v22.5.0 + */ + const WebSocket: typeof import("undici-types").WebSocket; + /** + * @since v22.5.0 + */ + const CloseEvent: typeof import("undici-types").CloseEvent; + /** + * @since v22.5.0 + */ + const MessageEvent: typeof import("undici-types").MessageEvent; +} +declare module "node:http" { + export * from "http"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/http2.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/http2.d.ts new file mode 100644 index 00000000..0dcc1d90 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/http2.d.ts @@ -0,0 +1,2644 @@ +/** + * The `node:http2` module provides an implementation of the [HTTP/2](https://tools.ietf.org/html/rfc7540) protocol. + * It can be accessed using: + * + * ```js + * import http2 from 'node:http2'; + * ``` + * @since v8.4.0 + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/http2.js) + */ +declare module "http2" { + import { NonSharedBuffer } from "node:buffer"; + import EventEmitter = require("node:events"); + import * as fs from "node:fs"; + import * as net from "node:net"; + import * as stream from "node:stream"; + import * as tls from "node:tls"; + import * as url from "node:url"; + import { + IncomingHttpHeaders as Http1IncomingHttpHeaders, + IncomingMessage, + OutgoingHttpHeaders, + ServerResponse, + } from "node:http"; + export { OutgoingHttpHeaders } from "node:http"; + export interface IncomingHttpStatusHeader { + ":status"?: number | undefined; + } + export interface IncomingHttpHeaders extends Http1IncomingHttpHeaders { + ":path"?: string | undefined; + ":method"?: string | undefined; + ":authority"?: string | undefined; + ":scheme"?: string | undefined; + } + // Http2Stream + export interface StreamPriorityOptions { + exclusive?: boolean | undefined; + parent?: number | undefined; + weight?: number | undefined; + silent?: boolean | undefined; + } + export interface StreamState { + localWindowSize?: number | undefined; + state?: number | undefined; + localClose?: number | undefined; + remoteClose?: number | undefined; + sumDependencyWeight?: number | undefined; + weight?: number | undefined; + } + export interface ServerStreamResponseOptions { + endStream?: boolean | undefined; + waitForTrailers?: boolean | undefined; + } + export interface StatOptions { + offset: number; + length: number; + } + export interface ServerStreamFileResponseOptions { + statCheck?: + | ((stats: fs.Stats, headers: OutgoingHttpHeaders, statOptions: StatOptions) => void) + | undefined; + waitForTrailers?: boolean | undefined; + offset?: number | undefined; + length?: number | undefined; + } + export interface ServerStreamFileResponseOptionsWithError extends ServerStreamFileResponseOptions { + onError?: ((err: NodeJS.ErrnoException) => void) | undefined; + } + export interface Http2Stream extends stream.Duplex { + /** + * Set to `true` if the `Http2Stream` instance was aborted abnormally. When set, + * the `'aborted'` event will have been emitted. + * @since v8.4.0 + */ + readonly aborted: boolean; + /** + * This property shows the number of characters currently buffered to be written. + * See `net.Socket.bufferSize` for details. + * @since v11.2.0, v10.16.0 + */ + readonly bufferSize: number; + /** + * Set to `true` if the `Http2Stream` instance has been closed. + * @since v9.4.0 + */ + readonly closed: boolean; + /** + * Set to `true` if the `Http2Stream` instance has been destroyed and is no longer + * usable. + * @since v8.4.0 + */ + readonly destroyed: boolean; + /** + * Set to `true` if the `END_STREAM` flag was set in the request or response + * HEADERS frame received, indicating that no additional data should be received + * and the readable side of the `Http2Stream` will be closed. + * @since v10.11.0 + */ + readonly endAfterHeaders: boolean; + /** + * The numeric stream identifier of this `Http2Stream` instance. Set to `undefined` if the stream identifier has not yet been assigned. + * @since v8.4.0 + */ + readonly id?: number | undefined; + /** + * Set to `true` if the `Http2Stream` instance has not yet been assigned a + * numeric stream identifier. + * @since v9.4.0 + */ + readonly pending: boolean; + /** + * Set to the `RST_STREAM` `error code` reported when the `Http2Stream` is + * destroyed after either receiving an `RST_STREAM` frame from the connected peer, + * calling `http2stream.close()`, or `http2stream.destroy()`. Will be `undefined` if the `Http2Stream` has not been closed. + * @since v8.4.0 + */ + readonly rstCode: number; + /** + * An object containing the outbound headers sent for this `Http2Stream`. + * @since v9.5.0 + */ + readonly sentHeaders: OutgoingHttpHeaders; + /** + * An array of objects containing the outbound informational (additional) headers + * sent for this `Http2Stream`. + * @since v9.5.0 + */ + readonly sentInfoHeaders?: OutgoingHttpHeaders[] | undefined; + /** + * An object containing the outbound trailers sent for this `HttpStream`. + * @since v9.5.0 + */ + readonly sentTrailers?: OutgoingHttpHeaders | undefined; + /** + * A reference to the `Http2Session` instance that owns this `Http2Stream`. The + * value will be `undefined` after the `Http2Stream` instance is destroyed. + * @since v8.4.0 + */ + readonly session: Http2Session | undefined; + /** + * Provides miscellaneous information about the current state of the `Http2Stream`. + * + * A current state of this `Http2Stream`. + * @since v8.4.0 + */ + readonly state: StreamState; + /** + * Closes the `Http2Stream` instance by sending an `RST_STREAM` frame to the + * connected HTTP/2 peer. + * @since v8.4.0 + * @param [code=http2.constants.NGHTTP2_NO_ERROR] Unsigned 32-bit integer identifying the error code. + * @param callback An optional function registered to listen for the `'close'` event. + */ + close(code?: number, callback?: () => void): void; + /** + * Updates the priority for this `Http2Stream` instance. + * @since v8.4.0 + */ + priority(options: StreamPriorityOptions): void; + /** + * ```js + * import http2 from 'node:http2'; + * const client = http2.connect('http://example.org:8000'); + * const { NGHTTP2_CANCEL } = http2.constants; + * const req = client.request({ ':path': '/' }); + * + * // Cancel the stream if there's no activity after 5 seconds + * req.setTimeout(5000, () => req.close(NGHTTP2_CANCEL)); + * ``` + * @since v8.4.0 + */ + setTimeout(msecs: number, callback?: () => void): void; + /** + * Sends a trailing `HEADERS` frame to the connected HTTP/2 peer. This method + * will cause the `Http2Stream` to be immediately closed and must only be + * called after the `'wantTrailers'` event has been emitted. When sending a + * request or sending a response, the `options.waitForTrailers` option must be set + * in order to keep the `Http2Stream` open after the final `DATA` frame so that + * trailers can be sent. + * + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * stream.respond(undefined, { waitForTrailers: true }); + * stream.on('wantTrailers', () => { + * stream.sendTrailers({ xyz: 'abc' }); + * }); + * stream.end('Hello World'); + * }); + * ``` + * + * The HTTP/1 specification forbids trailers from containing HTTP/2 pseudo-header + * fields (e.g. `':method'`, `':path'`, etc). + * @since v10.0.0 + */ + sendTrailers(headers: OutgoingHttpHeaders): void; + addListener(event: "aborted", listener: () => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "end", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "finish", listener: () => void): this; + addListener(event: "frameError", listener: (frameType: number, errorCode: number) => void): this; + addListener(event: "pipe", listener: (src: stream.Readable) => void): this; + addListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + addListener(event: "streamClosed", listener: (code: number) => void): this; + addListener(event: "timeout", listener: () => void): this; + addListener(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): this; + addListener(event: "wantTrailers", listener: () => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "aborted"): boolean; + emit(event: "close"): boolean; + emit(event: "data", chunk: NonSharedBuffer | string): boolean; + emit(event: "drain"): boolean; + emit(event: "end"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "finish"): boolean; + emit(event: "frameError", frameType: number, errorCode: number): boolean; + emit(event: "pipe", src: stream.Readable): boolean; + emit(event: "unpipe", src: stream.Readable): boolean; + emit(event: "streamClosed", code: number): boolean; + emit(event: "timeout"): boolean; + emit(event: "trailers", trailers: IncomingHttpHeaders, flags: number): boolean; + emit(event: "wantTrailers"): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "aborted", listener: () => void): this; + on(event: "close", listener: () => void): this; + on(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + on(event: "drain", listener: () => void): this; + on(event: "end", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "finish", listener: () => void): this; + on(event: "frameError", listener: (frameType: number, errorCode: number) => void): this; + on(event: "pipe", listener: (src: stream.Readable) => void): this; + on(event: "unpipe", listener: (src: stream.Readable) => void): this; + on(event: "streamClosed", listener: (code: number) => void): this; + on(event: "timeout", listener: () => void): this; + on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): this; + on(event: "wantTrailers", listener: () => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "aborted", listener: () => void): this; + once(event: "close", listener: () => void): this; + once(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + once(event: "drain", listener: () => void): this; + once(event: "end", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "finish", listener: () => void): this; + once(event: "frameError", listener: (frameType: number, errorCode: number) => void): this; + once(event: "pipe", listener: (src: stream.Readable) => void): this; + once(event: "unpipe", listener: (src: stream.Readable) => void): this; + once(event: "streamClosed", listener: (code: number) => void): this; + once(event: "timeout", listener: () => void): this; + once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): this; + once(event: "wantTrailers", listener: () => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "aborted", listener: () => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "end", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "finish", listener: () => void): this; + prependListener(event: "frameError", listener: (frameType: number, errorCode: number) => void): this; + prependListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependListener(event: "streamClosed", listener: (code: number) => void): this; + prependListener(event: "timeout", listener: () => void): this; + prependListener(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): this; + prependListener(event: "wantTrailers", listener: () => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "aborted", listener: () => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "end", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "finish", listener: () => void): this; + prependOnceListener(event: "frameError", listener: (frameType: number, errorCode: number) => void): this; + prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: "streamClosed", listener: (code: number) => void): this; + prependOnceListener(event: "timeout", listener: () => void): this; + prependOnceListener(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): this; + prependOnceListener(event: "wantTrailers", listener: () => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + export interface ClientHttp2Stream extends Http2Stream { + addListener(event: "continue", listener: () => {}): this; + addListener( + event: "headers", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + addListener(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): this; + addListener( + event: "response", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "continue"): boolean; + emit(event: "headers", headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number): boolean; + emit(event: "push", headers: IncomingHttpHeaders, flags: number): boolean; + emit(event: "response", headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "continue", listener: () => {}): this; + on( + event: "headers", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + on(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): this; + on( + event: "response", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "continue", listener: () => {}): this; + once( + event: "headers", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): this; + once( + event: "response", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "continue", listener: () => {}): this; + prependListener( + event: "headers", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + prependListener(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): this; + prependListener( + event: "response", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "continue", listener: () => {}): this; + prependOnceListener( + event: "headers", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + prependOnceListener(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): this; + prependOnceListener( + event: "response", + listener: (headers: IncomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void, + ): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + export interface ServerHttp2Stream extends Http2Stream { + /** + * True if headers were sent, false otherwise (read-only). + * @since v8.4.0 + */ + readonly headersSent: boolean; + /** + * Read-only property mapped to the `SETTINGS_ENABLE_PUSH` flag of the remote + * client's most recent `SETTINGS` frame. Will be `true` if the remote peer + * accepts push streams, `false` otherwise. Settings are the same for every `Http2Stream` in the same `Http2Session`. + * @since v8.4.0 + */ + readonly pushAllowed: boolean; + /** + * Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer. + * @since v8.4.0 + */ + additionalHeaders(headers: OutgoingHttpHeaders): void; + /** + * Initiates a push stream. The callback is invoked with the new `Http2Stream` instance created for the push stream passed as the second argument, or an `Error` passed as the first argument. + * + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * stream.respond({ ':status': 200 }); + * stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => { + * if (err) throw err; + * pushStream.respond({ ':status': 200 }); + * pushStream.end('some pushed data'); + * }); + * stream.end('some data'); + * }); + * ``` + * + * Setting the weight of a push stream is not allowed in the `HEADERS` frame. Pass + * a `weight` value to `http2stream.priority` with the `silent` option set to `true` to enable server-side bandwidth balancing between concurrent streams. + * + * Calling `http2stream.pushStream()` from within a pushed stream is not permitted + * and will throw an error. + * @since v8.4.0 + * @param callback Callback that is called once the push stream has been initiated. + */ + pushStream( + headers: OutgoingHttpHeaders, + callback?: (err: Error | null, pushStream: ServerHttp2Stream, headers: OutgoingHttpHeaders) => void, + ): void; + pushStream( + headers: OutgoingHttpHeaders, + options?: StreamPriorityOptions, + callback?: (err: Error | null, pushStream: ServerHttp2Stream, headers: OutgoingHttpHeaders) => void, + ): void; + /** + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * stream.respond({ ':status': 200 }); + * stream.end('some data'); + * }); + * ``` + * + * Initiates a response. When the `options.waitForTrailers` option is set, the `'wantTrailers'` event + * will be emitted immediately after queuing the last chunk of payload data to be sent. + * The `http2stream.sendTrailers()` method can then be used to send trailing header fields to the peer. + * + * When `options.waitForTrailers` is set, the `Http2Stream` will not automatically + * close when the final `DATA` frame is transmitted. User code must call either `http2stream.sendTrailers()` or `http2stream.close()` to close the `Http2Stream`. + * + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * stream.respond({ ':status': 200 }, { waitForTrailers: true }); + * stream.on('wantTrailers', () => { + * stream.sendTrailers({ ABC: 'some value to send' }); + * }); + * stream.end('some data'); + * }); + * ``` + * @since v8.4.0 + */ + respond(headers?: OutgoingHttpHeaders, options?: ServerStreamResponseOptions): void; + /** + * Initiates a response whose data is read from the given file descriptor. No + * validation is performed on the given file descriptor. If an error occurs while + * attempting to read data using the file descriptor, the `Http2Stream` will be + * closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR` code. + * + * When used, the `Http2Stream` object's `Duplex` interface will be closed + * automatically. + * + * ```js + * import http2 from 'node:http2'; + * import fs from 'node:fs'; + * + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * const fd = fs.openSync('/some/file', 'r'); + * + * const stat = fs.fstatSync(fd); + * const headers = { + * 'content-length': stat.size, + * 'last-modified': stat.mtime.toUTCString(), + * 'content-type': 'text/plain; charset=utf-8', + * }; + * stream.respondWithFD(fd, headers); + * stream.on('close', () => fs.closeSync(fd)); + * }); + * ``` + * + * The optional `options.statCheck` function may be specified to give user code + * an opportunity to set additional content headers based on the `fs.Stat` details + * of the given fd. If the `statCheck` function is provided, the `http2stream.respondWithFD()` method will + * perform an `fs.fstat()` call to collect details on the provided file descriptor. + * + * The `offset` and `length` options may be used to limit the response to a + * specific range subset. This can be used, for instance, to support HTTP Range + * requests. + * + * The file descriptor or `FileHandle` is not closed when the stream is closed, + * so it will need to be closed manually once it is no longer needed. + * Using the same file descriptor concurrently for multiple streams + * is not supported and may result in data loss. Re-using a file descriptor + * after a stream has finished is supported. + * + * When the `options.waitForTrailers` option is set, the `'wantTrailers'` event + * will be emitted immediately after queuing the last chunk of payload data to be + * sent. The `http2stream.sendTrailers()` method can then be used to sent trailing + * header fields to the peer. + * + * When `options.waitForTrailers` is set, the `Http2Stream` will not automatically + * close when the final `DATA` frame is transmitted. User code _must_ call either `http2stream.sendTrailers()` + * or `http2stream.close()` to close the `Http2Stream`. + * + * ```js + * import http2 from 'node:http2'; + * import fs from 'node:fs'; + * + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * const fd = fs.openSync('/some/file', 'r'); + * + * const stat = fs.fstatSync(fd); + * const headers = { + * 'content-length': stat.size, + * 'last-modified': stat.mtime.toUTCString(), + * 'content-type': 'text/plain; charset=utf-8', + * }; + * stream.respondWithFD(fd, headers, { waitForTrailers: true }); + * stream.on('wantTrailers', () => { + * stream.sendTrailers({ ABC: 'some value to send' }); + * }); + * + * stream.on('close', () => fs.closeSync(fd)); + * }); + * ``` + * @since v8.4.0 + * @param fd A readable file descriptor. + */ + respondWithFD( + fd: number | fs.promises.FileHandle, + headers?: OutgoingHttpHeaders, + options?: ServerStreamFileResponseOptions, + ): void; + /** + * Sends a regular file as the response. The `path` must specify a regular file + * or an `'error'` event will be emitted on the `Http2Stream` object. + * + * When used, the `Http2Stream` object's `Duplex` interface will be closed + * automatically. + * + * The optional `options.statCheck` function may be specified to give user code + * an opportunity to set additional content headers based on the `fs.Stat` details + * of the given file: + * + * If an error occurs while attempting to read the file data, the `Http2Stream` will be closed using an + * `RST_STREAM` frame using the standard `INTERNAL_ERROR` code. + * If the `onError` callback is defined, then it will be called. Otherwise, the stream will be destroyed. + * + * Example using a file path: + * + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * function statCheck(stat, headers) { + * headers['last-modified'] = stat.mtime.toUTCString(); + * } + * + * function onError(err) { + * // stream.respond() can throw if the stream has been destroyed by + * // the other side. + * try { + * if (err.code === 'ENOENT') { + * stream.respond({ ':status': 404 }); + * } else { + * stream.respond({ ':status': 500 }); + * } + * } catch (err) { + * // Perform actual error handling. + * console.error(err); + * } + * stream.end(); + * } + * + * stream.respondWithFile('/some/file', + * { 'content-type': 'text/plain; charset=utf-8' }, + * { statCheck, onError }); + * }); + * ``` + * + * The `options.statCheck` function may also be used to cancel the send operation + * by returning `false`. For instance, a conditional request may check the stat + * results to determine if the file has been modified to return an appropriate `304` response: + * + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * function statCheck(stat, headers) { + * // Check the stat here... + * stream.respond({ ':status': 304 }); + * return false; // Cancel the send operation + * } + * stream.respondWithFile('/some/file', + * { 'content-type': 'text/plain; charset=utf-8' }, + * { statCheck }); + * }); + * ``` + * + * The `content-length` header field will be automatically set. + * + * The `offset` and `length` options may be used to limit the response to a + * specific range subset. This can be used, for instance, to support HTTP Range + * requests. + * + * The `options.onError` function may also be used to handle all the errors + * that could happen before the delivery of the file is initiated. The + * default behavior is to destroy the stream. + * + * When the `options.waitForTrailers` option is set, the `'wantTrailers'` event + * will be emitted immediately after queuing the last chunk of payload data to be + * sent. The `http2stream.sendTrailers()` method can then be used to sent trailing + * header fields to the peer. + * + * When `options.waitForTrailers` is set, the `Http2Stream` will not automatically + * close when the final `DATA` frame is transmitted. User code must call either`http2stream.sendTrailers()` or `http2stream.close()` to close the`Http2Stream`. + * + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer(); + * server.on('stream', (stream) => { + * stream.respondWithFile('/some/file', + * { 'content-type': 'text/plain; charset=utf-8' }, + * { waitForTrailers: true }); + * stream.on('wantTrailers', () => { + * stream.sendTrailers({ ABC: 'some value to send' }); + * }); + * }); + * ``` + * @since v8.4.0 + */ + respondWithFile( + path: string, + headers?: OutgoingHttpHeaders, + options?: ServerStreamFileResponseOptionsWithError, + ): void; + } + // Http2Session + export interface Settings { + headerTableSize?: number | undefined; + enablePush?: boolean | undefined; + initialWindowSize?: number | undefined; + maxFrameSize?: number | undefined; + maxConcurrentStreams?: number | undefined; + maxHeaderListSize?: number | undefined; + enableConnectProtocol?: boolean | undefined; + } + export interface ClientSessionRequestOptions { + endStream?: boolean | undefined; + exclusive?: boolean | undefined; + parent?: number | undefined; + weight?: number | undefined; + waitForTrailers?: boolean | undefined; + signal?: AbortSignal | undefined; + } + export interface SessionState { + effectiveLocalWindowSize?: number | undefined; + effectiveRecvDataLength?: number | undefined; + nextStreamID?: number | undefined; + localWindowSize?: number | undefined; + lastProcStreamID?: number | undefined; + remoteWindowSize?: number | undefined; + outboundQueueSize?: number | undefined; + deflateDynamicTableSize?: number | undefined; + inflateDynamicTableSize?: number | undefined; + } + export interface Http2Session extends EventEmitter { + /** + * Value will be `undefined` if the `Http2Session` is not yet connected to a + * socket, `h2c` if the `Http2Session` is not connected to a `TLSSocket`, or + * will return the value of the connected `TLSSocket`'s own `alpnProtocol` property. + * @since v9.4.0 + */ + readonly alpnProtocol?: string | undefined; + /** + * Will be `true` if this `Http2Session` instance has been closed, otherwise `false`. + * @since v9.4.0 + */ + readonly closed: boolean; + /** + * Will be `true` if this `Http2Session` instance is still connecting, will be set + * to `false` before emitting `connect` event and/or calling the `http2.connect` callback. + * @since v10.0.0 + */ + readonly connecting: boolean; + /** + * Will be `true` if this `Http2Session` instance has been destroyed and must no + * longer be used, otherwise `false`. + * @since v8.4.0 + */ + readonly destroyed: boolean; + /** + * Value is `undefined` if the `Http2Session` session socket has not yet been + * connected, `true` if the `Http2Session` is connected with a `TLSSocket`, + * and `false` if the `Http2Session` is connected to any other kind of socket + * or stream. + * @since v9.4.0 + */ + readonly encrypted?: boolean | undefined; + /** + * A prototype-less object describing the current local settings of this `Http2Session`. + * The local settings are local to _this_`Http2Session` instance. + * @since v8.4.0 + */ + readonly localSettings: Settings; + /** + * If the `Http2Session` is connected to a `TLSSocket`, the `originSet` property + * will return an `Array` of origins for which the `Http2Session` may be + * considered authoritative. + * + * The `originSet` property is only available when using a secure TLS connection. + * @since v9.4.0 + */ + readonly originSet?: string[] | undefined; + /** + * Indicates whether the `Http2Session` is currently waiting for acknowledgment of + * a sent `SETTINGS` frame. Will be `true` after calling the `http2session.settings()` method. + * Will be `false` once all sent `SETTINGS` frames have been acknowledged. + * @since v8.4.0 + */ + readonly pendingSettingsAck: boolean; + /** + * A prototype-less object describing the current remote settings of this`Http2Session`. + * The remote settings are set by the _connected_ HTTP/2 peer. + * @since v8.4.0 + */ + readonly remoteSettings: Settings; + /** + * Returns a `Proxy` object that acts as a `net.Socket` (or `tls.TLSSocket`) but + * limits available methods to ones safe to use with HTTP/2. + * + * `destroy`, `emit`, `end`, `pause`, `read`, `resume`, and `write` will throw + * an error with code `ERR_HTTP2_NO_SOCKET_MANIPULATION`. See `Http2Session and Sockets` for more information. + * + * `setTimeout` method will be called on this `Http2Session`. + * + * All other interactions will be routed directly to the socket. + * @since v8.4.0 + */ + readonly socket: net.Socket | tls.TLSSocket; + /** + * Provides miscellaneous information about the current state of the`Http2Session`. + * + * An object describing the current status of this `Http2Session`. + * @since v8.4.0 + */ + readonly state: SessionState; + /** + * The `http2session.type` will be equal to `http2.constants.NGHTTP2_SESSION_SERVER` if this `Http2Session` instance is a + * server, and `http2.constants.NGHTTP2_SESSION_CLIENT` if the instance is a + * client. + * @since v8.4.0 + */ + readonly type: number; + /** + * Gracefully closes the `Http2Session`, allowing any existing streams to + * complete on their own and preventing new `Http2Stream` instances from being + * created. Once closed, `http2session.destroy()`_might_ be called if there + * are no open `Http2Stream` instances. + * + * If specified, the `callback` function is registered as a handler for the`'close'` event. + * @since v9.4.0 + */ + close(callback?: () => void): void; + /** + * Immediately terminates the `Http2Session` and the associated `net.Socket` or `tls.TLSSocket`. + * + * Once destroyed, the `Http2Session` will emit the `'close'` event. If `error` is not undefined, an `'error'` event will be emitted immediately before the `'close'` event. + * + * If there are any remaining open `Http2Streams` associated with the `Http2Session`, those will also be destroyed. + * @since v8.4.0 + * @param error An `Error` object if the `Http2Session` is being destroyed due to an error. + * @param code The HTTP/2 error code to send in the final `GOAWAY` frame. If unspecified, and `error` is not undefined, the default is `INTERNAL_ERROR`, otherwise defaults to `NO_ERROR`. + */ + destroy(error?: Error, code?: number): void; + /** + * Transmits a `GOAWAY` frame to the connected peer _without_ shutting down the`Http2Session`. + * @since v9.4.0 + * @param code An HTTP/2 error code + * @param lastStreamID The numeric ID of the last processed `Http2Stream` + * @param opaqueData A `TypedArray` or `DataView` instance containing additional data to be carried within the `GOAWAY` frame. + */ + goaway(code?: number, lastStreamID?: number, opaqueData?: NodeJS.ArrayBufferView): void; + /** + * Sends a `PING` frame to the connected HTTP/2 peer. A `callback` function must + * be provided. The method will return `true` if the `PING` was sent, `false` otherwise. + * + * The maximum number of outstanding (unacknowledged) pings is determined by the `maxOutstandingPings` configuration option. The default maximum is 10. + * + * If provided, the `payload` must be a `Buffer`, `TypedArray`, or `DataView` containing 8 bytes of data that will be transmitted with the `PING` and + * returned with the ping acknowledgment. + * + * The callback will be invoked with three arguments: an error argument that will + * be `null` if the `PING` was successfully acknowledged, a `duration` argument + * that reports the number of milliseconds elapsed since the ping was sent and the + * acknowledgment was received, and a `Buffer` containing the 8-byte `PING` payload. + * + * ```js + * session.ping(Buffer.from('abcdefgh'), (err, duration, payload) => { + * if (!err) { + * console.log(`Ping acknowledged in ${duration} milliseconds`); + * console.log(`With payload '${payload.toString()}'`); + * } + * }); + * ``` + * + * If the `payload` argument is not specified, the default payload will be the + * 64-bit timestamp (little endian) marking the start of the `PING` duration. + * @since v8.9.3 + * @param payload Optional ping payload. + */ + ping(callback: (err: Error | null, duration: number, payload: NonSharedBuffer) => void): boolean; + ping( + payload: NodeJS.ArrayBufferView, + callback: (err: Error | null, duration: number, payload: NonSharedBuffer) => void, + ): boolean; + /** + * Calls `ref()` on this `Http2Session` instance's underlying `net.Socket`. + * @since v9.4.0 + */ + ref(): void; + /** + * Sets the local endpoint's window size. + * The `windowSize` is the total window size to set, not + * the delta. + * + * ```js + * import http2 from 'node:http2'; + * + * const server = http2.createServer(); + * const expectedWindowSize = 2 ** 20; + * server.on('connect', (session) => { + * + * // Set local window size to be 2 ** 20 + * session.setLocalWindowSize(expectedWindowSize); + * }); + * ``` + * @since v15.3.0, v14.18.0 + */ + setLocalWindowSize(windowSize: number): void; + /** + * Used to set a callback function that is called when there is no activity on + * the `Http2Session` after `msecs` milliseconds. The given `callback` is + * registered as a listener on the `'timeout'` event. + * @since v8.4.0 + */ + setTimeout(msecs: number, callback?: () => void): void; + /** + * Updates the current local settings for this `Http2Session` and sends a new `SETTINGS` frame to the connected HTTP/2 peer. + * + * Once called, the `http2session.pendingSettingsAck` property will be `true` while the session is waiting for the remote peer to acknowledge the new + * settings. + * + * The new settings will not become effective until the `SETTINGS` acknowledgment + * is received and the `'localSettings'` event is emitted. It is possible to send + * multiple `SETTINGS` frames while acknowledgment is still pending. + * @since v8.4.0 + * @param callback Callback that is called once the session is connected or right away if the session is already connected. + */ + settings( + settings: Settings, + callback?: (err: Error | null, settings: Settings, duration: number) => void, + ): void; + /** + * Calls `unref()` on this `Http2Session`instance's underlying `net.Socket`. + * @since v9.4.0 + */ + unref(): void; + addListener(event: "close", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener( + event: "frameError", + listener: (frameType: number, errorCode: number, streamID: number) => void, + ): this; + addListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: NonSharedBuffer) => void, + ): this; + addListener(event: "localSettings", listener: (settings: Settings) => void): this; + addListener(event: "ping", listener: () => void): this; + addListener(event: "remoteSettings", listener: (settings: Settings) => void): this; + addListener(event: "timeout", listener: () => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "close"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "frameError", frameType: number, errorCode: number, streamID: number): boolean; + emit(event: "goaway", errorCode: number, lastStreamID: number, opaqueData?: NonSharedBuffer): boolean; + emit(event: "localSettings", settings: Settings): boolean; + emit(event: "ping"): boolean; + emit(event: "remoteSettings", settings: Settings): boolean; + emit(event: "timeout"): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "close", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "frameError", listener: (frameType: number, errorCode: number, streamID: number) => void): this; + on( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: NonSharedBuffer) => void, + ): this; + on(event: "localSettings", listener: (settings: Settings) => void): this; + on(event: "ping", listener: () => void): this; + on(event: "remoteSettings", listener: (settings: Settings) => void): this; + on(event: "timeout", listener: () => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "frameError", listener: (frameType: number, errorCode: number, streamID: number) => void): this; + once( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: NonSharedBuffer) => void, + ): this; + once(event: "localSettings", listener: (settings: Settings) => void): this; + once(event: "ping", listener: () => void): this; + once(event: "remoteSettings", listener: (settings: Settings) => void): this; + once(event: "timeout", listener: () => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener( + event: "frameError", + listener: (frameType: number, errorCode: number, streamID: number) => void, + ): this; + prependListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: NonSharedBuffer) => void, + ): this; + prependListener(event: "localSettings", listener: (settings: Settings) => void): this; + prependListener(event: "ping", listener: () => void): this; + prependListener(event: "remoteSettings", listener: (settings: Settings) => void): this; + prependListener(event: "timeout", listener: () => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener( + event: "frameError", + listener: (frameType: number, errorCode: number, streamID: number) => void, + ): this; + prependOnceListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: NonSharedBuffer) => void, + ): this; + prependOnceListener(event: "localSettings", listener: (settings: Settings) => void): this; + prependOnceListener(event: "ping", listener: () => void): this; + prependOnceListener(event: "remoteSettings", listener: (settings: Settings) => void): this; + prependOnceListener(event: "timeout", listener: () => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + export interface ClientHttp2Session extends Http2Session { + /** + * For HTTP/2 Client `Http2Session` instances only, the `http2session.request()` creates and returns an `Http2Stream` instance that can be used to send an + * HTTP/2 request to the connected server. + * + * When a `ClientHttp2Session` is first created, the socket may not yet be + * connected. if `clienthttp2session.request()` is called during this time, the + * actual request will be deferred until the socket is ready to go. + * If the `session` is closed before the actual request be executed, an `ERR_HTTP2_GOAWAY_SESSION` is thrown. + * + * This method is only available if `http2session.type` is equal to `http2.constants.NGHTTP2_SESSION_CLIENT`. + * + * ```js + * import http2 from 'node:http2'; + * const clientSession = http2.connect('https://localhost:1234'); + * const { + * HTTP2_HEADER_PATH, + * HTTP2_HEADER_STATUS, + * } = http2.constants; + * + * const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' }); + * req.on('response', (headers) => { + * console.log(headers[HTTP2_HEADER_STATUS]); + * req.on('data', (chunk) => { // .. }); + * req.on('end', () => { // .. }); + * }); + * ``` + * + * When the `options.waitForTrailers` option is set, the `'wantTrailers'` event + * is emitted immediately after queuing the last chunk of payload data to be sent. + * The `http2stream.sendTrailers()` method can then be called to send trailing + * headers to the peer. + * + * When `options.waitForTrailers` is set, the `Http2Stream` will not automatically + * close when the final `DATA` frame is transmitted. User code must call either`http2stream.sendTrailers()` or `http2stream.close()` to close the`Http2Stream`. + * + * When `options.signal` is set with an `AbortSignal` and then `abort` on the + * corresponding `AbortController` is called, the request will emit an `'error'`event with an `AbortError` error. + * + * The `:method` and `:path` pseudo-headers are not specified within `headers`, + * they respectively default to: + * + * * `:method` \= `'GET'` + * * `:path` \= `/` + * @since v8.4.0 + */ + request( + headers?: OutgoingHttpHeaders | readonly string[], + options?: ClientSessionRequestOptions, + ): ClientHttp2Stream; + addListener(event: "altsvc", listener: (alt: string, origin: string, stream: number) => void): this; + addListener(event: "origin", listener: (origins: string[]) => void): this; + addListener( + event: "connect", + listener: (session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket) => void, + ): this; + addListener( + event: "stream", + listener: ( + stream: ClientHttp2Stream, + headers: IncomingHttpHeaders & IncomingHttpStatusHeader, + flags: number, + ) => void, + ): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "altsvc", alt: string, origin: string, stream: number): boolean; + emit(event: "origin", origins: readonly string[]): boolean; + emit(event: "connect", session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket): boolean; + emit( + event: "stream", + stream: ClientHttp2Stream, + headers: IncomingHttpHeaders & IncomingHttpStatusHeader, + flags: number, + ): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "altsvc", listener: (alt: string, origin: string, stream: number) => void): this; + on(event: "origin", listener: (origins: string[]) => void): this; + on(event: "connect", listener: (session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket) => void): this; + on( + event: "stream", + listener: ( + stream: ClientHttp2Stream, + headers: IncomingHttpHeaders & IncomingHttpStatusHeader, + flags: number, + ) => void, + ): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "altsvc", listener: (alt: string, origin: string, stream: number) => void): this; + once(event: "origin", listener: (origins: string[]) => void): this; + once( + event: "connect", + listener: (session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket) => void, + ): this; + once( + event: "stream", + listener: ( + stream: ClientHttp2Stream, + headers: IncomingHttpHeaders & IncomingHttpStatusHeader, + flags: number, + ) => void, + ): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "altsvc", listener: (alt: string, origin: string, stream: number) => void): this; + prependListener(event: "origin", listener: (origins: string[]) => void): this; + prependListener( + event: "connect", + listener: (session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket) => void, + ): this; + prependListener( + event: "stream", + listener: ( + stream: ClientHttp2Stream, + headers: IncomingHttpHeaders & IncomingHttpStatusHeader, + flags: number, + ) => void, + ): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "altsvc", listener: (alt: string, origin: string, stream: number) => void): this; + prependOnceListener(event: "origin", listener: (origins: string[]) => void): this; + prependOnceListener( + event: "connect", + listener: (session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket) => void, + ): this; + prependOnceListener( + event: "stream", + listener: ( + stream: ClientHttp2Stream, + headers: IncomingHttpHeaders & IncomingHttpStatusHeader, + flags: number, + ) => void, + ): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + export interface AlternativeServiceOptions { + origin: number | string | url.URL; + } + export interface ServerHttp2Session< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + > extends Http2Session { + readonly server: + | Http2Server + | Http2SecureServer; + /** + * Submits an `ALTSVC` frame (as defined by [RFC 7838](https://tools.ietf.org/html/rfc7838)) to the connected client. + * + * ```js + * import http2 from 'node:http2'; + * + * const server = http2.createServer(); + * server.on('session', (session) => { + * // Set altsvc for origin https://example.org:80 + * session.altsvc('h2=":8000"', 'https://example.org:80'); + * }); + * + * server.on('stream', (stream) => { + * // Set altsvc for a specific stream + * stream.session.altsvc('h2=":8000"', stream.id); + * }); + * ``` + * + * Sending an `ALTSVC` frame with a specific stream ID indicates that the alternate + * service is associated with the origin of the given `Http2Stream`. + * + * The `alt` and origin string _must_ contain only ASCII bytes and are + * strictly interpreted as a sequence of ASCII bytes. The special value `'clear'`may be passed to clear any previously set alternative service for a given + * domain. + * + * When a string is passed for the `originOrStream` argument, it will be parsed as + * a URL and the origin will be derived. For instance, the origin for the + * HTTP URL `'https://example.org/foo/bar'` is the ASCII string`'https://example.org'`. An error will be thrown if either the given string + * cannot be parsed as a URL or if a valid origin cannot be derived. + * + * A `URL` object, or any object with an `origin` property, may be passed as`originOrStream`, in which case the value of the `origin` property will be + * used. The value of the `origin` property _must_ be a properly serialized + * ASCII origin. + * @since v9.4.0 + * @param alt A description of the alternative service configuration as defined by `RFC 7838`. + * @param originOrStream Either a URL string specifying the origin (or an `Object` with an `origin` property) or the numeric identifier of an active `Http2Stream` as given by the + * `http2stream.id` property. + */ + altsvc(alt: string, originOrStream: number | string | url.URL | AlternativeServiceOptions): void; + /** + * Submits an `ORIGIN` frame (as defined by [RFC 8336](https://tools.ietf.org/html/rfc8336)) to the connected client + * to advertise the set of origins for which the server is capable of providing + * authoritative responses. + * + * ```js + * import http2 from 'node:http2'; + * const options = getSecureOptionsSomehow(); + * const server = http2.createSecureServer(options); + * server.on('stream', (stream) => { + * stream.respond(); + * stream.end('ok'); + * }); + * server.on('session', (session) => { + * session.origin('https://example.com', 'https://example.org'); + * }); + * ``` + * + * When a string is passed as an `origin`, it will be parsed as a URL and the + * origin will be derived. For instance, the origin for the HTTP URL `'https://example.org/foo/bar'` is the ASCII string` 'https://example.org'`. An error will be thrown if either the given + * string + * cannot be parsed as a URL or if a valid origin cannot be derived. + * + * A `URL` object, or any object with an `origin` property, may be passed as + * an `origin`, in which case the value of the `origin` property will be + * used. The value of the `origin` property _must_ be a properly serialized + * ASCII origin. + * + * Alternatively, the `origins` option may be used when creating a new HTTP/2 + * server using the `http2.createSecureServer()` method: + * + * ```js + * import http2 from 'node:http2'; + * const options = getSecureOptionsSomehow(); + * options.origins = ['https://example.com', 'https://example.org']; + * const server = http2.createSecureServer(options); + * server.on('stream', (stream) => { + * stream.respond(); + * stream.end('ok'); + * }); + * ``` + * @since v10.12.0 + * @param origins One or more URL Strings passed as separate arguments. + */ + origin( + ...origins: Array< + | string + | url.URL + | { + origin: string; + } + > + ): void; + addListener( + event: "connect", + listener: ( + session: ServerHttp2Session, + socket: net.Socket | tls.TLSSocket, + ) => void, + ): this; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit( + event: "connect", + session: ServerHttp2Session, + socket: net.Socket | tls.TLSSocket, + ): boolean; + emit(event: "stream", stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on( + event: "connect", + listener: ( + session: ServerHttp2Session, + socket: net.Socket | tls.TLSSocket, + ) => void, + ): this; + on( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once( + event: "connect", + listener: ( + session: ServerHttp2Session, + socket: net.Socket | tls.TLSSocket, + ) => void, + ): this; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener( + event: "connect", + listener: ( + session: ServerHttp2Session, + socket: net.Socket | tls.TLSSocket, + ) => void, + ): this; + prependListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener( + event: "connect", + listener: ( + session: ServerHttp2Session, + socket: net.Socket | tls.TLSSocket, + ) => void, + ): this; + prependOnceListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + // Http2Server + export interface SessionOptions { + /** + * Sets the maximum dynamic table size for deflating header fields. + * @default 4Kib + */ + maxDeflateDynamicTableSize?: number | undefined; + /** + * Sets the maximum number of settings entries per `SETTINGS` frame. + * The minimum value allowed is `1`. + * @default 32 + */ + maxSettings?: number | undefined; + /** + * Sets the maximum memory that the `Http2Session` is permitted to use. + * The value is expressed in terms of number of megabytes, e.g. `1` equal 1 megabyte. + * The minimum value allowed is `1`. + * This is a credit based limit, existing `Http2Stream`s may cause this limit to be exceeded, + * but new `Http2Stream` instances will be rejected while this limit is exceeded. + * The current number of `Http2Stream` sessions, the current memory use of the header compression tables, + * current data queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all counted towards the current limit. + * @default 10 + */ + maxSessionMemory?: number | undefined; + /** + * Sets the maximum number of header entries. + * This is similar to `server.maxHeadersCount` or `request.maxHeadersCount` in the `node:http` module. + * The minimum value is `1`. + * @default 128 + */ + maxHeaderListPairs?: number | undefined; + /** + * Sets the maximum number of outstanding, unacknowledged pings. + * @default 10 + */ + maxOutstandingPings?: number | undefined; + /** + * Sets the maximum allowed size for a serialized, compressed block of headers. + * Attempts to send headers that exceed this limit will result in + * a `'frameError'` event being emitted and the stream being closed and destroyed. + */ + maxSendHeaderBlockLength?: number | undefined; + /** + * Strategy used for determining the amount of padding to use for `HEADERS` and `DATA` frames. + * @default http2.constants.PADDING_STRATEGY_NONE + */ + paddingStrategy?: number | undefined; + /** + * Sets the maximum number of concurrent streams for the remote peer as if a `SETTINGS` frame had been received. + * Will be overridden if the remote peer sets its own value for `maxConcurrentStreams`. + * @default 100 + */ + peerMaxConcurrentStreams?: number | undefined; + /** + * The initial settings to send to the remote peer upon connection. + */ + settings?: Settings | undefined; + /** + * The array of integer values determines the settings types, + * which are included in the `CustomSettings`-property of the received remoteSettings. + * Please see the `CustomSettings`-property of the `Http2Settings` object for more information, on the allowed setting types. + */ + remoteCustomSettings?: number[] | undefined; + /** + * Specifies a timeout in milliseconds that + * a server should wait when an [`'unknownProtocol'`][] is emitted. If the + * socket has not been destroyed by that time the server will destroy it. + * @default 100000 + */ + unknownProtocolTimeout?: number | undefined; + /** + * If `true`, it turns on strict leading + * and trailing whitespace validation for HTTP/2 header field names and values + * as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1). + * @since v24.2.0 + * @default true + */ + strictFieldWhitespaceValidation?: boolean | undefined; + } + export interface ClientSessionOptions extends SessionOptions { + /** + * Sets the maximum number of reserved push streams the client will accept at any given time. + * Once the current number of currently reserved push streams exceeds reaches this limit, + * new push streams sent by the server will be automatically rejected. + * The minimum allowed value is 0. The maximum allowed value is 232-1. + * A negative value sets this option to the maximum allowed value. + * @default 200 + */ + maxReservedRemoteStreams?: number | undefined; + /** + * An optional callback that receives the `URL` instance passed to `connect` and the `options` object, + * and returns any `Duplex` stream that is to be used as the connection for this session. + */ + createConnection?: ((authority: url.URL, option: SessionOptions) => stream.Duplex) | undefined; + /** + * The protocol to connect with, if not set in the `authority`. + * Value may be either `'http:'` or `'https:'`. + * @default 'https:' + */ + protocol?: "http:" | "https:" | undefined; + } + export interface ServerSessionOptions< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + > extends SessionOptions { + streamResetBurst?: number | undefined; + streamResetRate?: number | undefined; + Http1IncomingMessage?: Http1Request | undefined; + Http1ServerResponse?: Http1Response | undefined; + Http2ServerRequest?: Http2Request | undefined; + Http2ServerResponse?: Http2Response | undefined; + } + export interface SecureClientSessionOptions extends ClientSessionOptions, tls.ConnectionOptions {} + export interface SecureServerSessionOptions< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + > extends ServerSessionOptions, tls.TlsOptions {} + export interface ServerOptions< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + > extends ServerSessionOptions {} + export interface SecureServerOptions< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + > extends SecureServerSessionOptions { + allowHTTP1?: boolean | undefined; + origins?: string[] | undefined; + } + interface HTTP2ServerCommon { + setTimeout(msec?: number, callback?: () => void): this; + /** + * Throws ERR_HTTP2_INVALID_SETTING_VALUE for invalid settings values. + * Throws ERR_INVALID_ARG_TYPE for invalid settings argument. + */ + updateSettings(settings: Settings): void; + } + export interface Http2Server< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + > extends net.Server, HTTP2ServerCommon { + addListener( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + addListener( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + addListener( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + addListener(event: "sessionError", listener: (err: Error) => void): this; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + addListener(event: "timeout", listener: () => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit( + event: "checkContinue", + request: InstanceType, + response: InstanceType, + ): boolean; + emit(event: "request", request: InstanceType, response: InstanceType): boolean; + emit( + event: "session", + session: ServerHttp2Session, + ): boolean; + emit(event: "sessionError", err: Error): boolean; + emit(event: "stream", stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number): boolean; + emit(event: "timeout"): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + on( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + on( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + on(event: "sessionError", listener: (err: Error) => void): this; + on( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + on(event: "timeout", listener: () => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + once( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + once( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + once(event: "sessionError", listener: (err: Error) => void): this; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + once(event: "timeout", listener: () => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependListener( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependListener( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + prependListener(event: "sessionError", listener: (err: Error) => void): this; + prependListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + prependListener(event: "timeout", listener: () => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependOnceListener( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependOnceListener( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + prependOnceListener(event: "sessionError", listener: (err: Error) => void): this; + prependOnceListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + prependOnceListener(event: "timeout", listener: () => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + export interface Http2SecureServer< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + > extends tls.Server, HTTP2ServerCommon { + addListener( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + addListener( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + addListener( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + addListener(event: "sessionError", listener: (err: Error) => void): this; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + addListener(event: "timeout", listener: () => void): this; + addListener(event: "unknownProtocol", listener: (socket: tls.TLSSocket) => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit( + event: "checkContinue", + request: InstanceType, + response: InstanceType, + ): boolean; + emit(event: "request", request: InstanceType, response: InstanceType): boolean; + emit( + event: "session", + session: ServerHttp2Session, + ): boolean; + emit(event: "sessionError", err: Error): boolean; + emit(event: "stream", stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number): boolean; + emit(event: "timeout"): boolean; + emit(event: "unknownProtocol", socket: tls.TLSSocket): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + on( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + on( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + on(event: "sessionError", listener: (err: Error) => void): this; + on( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + on(event: "timeout", listener: () => void): this; + on(event: "unknownProtocol", listener: (socket: tls.TLSSocket) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + once( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + once( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + once(event: "sessionError", listener: (err: Error) => void): this; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + once(event: "timeout", listener: () => void): this; + once(event: "unknownProtocol", listener: (socket: tls.TLSSocket) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependListener( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependListener( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + prependListener(event: "sessionError", listener: (err: Error) => void): this; + prependListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + prependListener(event: "timeout", listener: () => void): this; + prependListener(event: "unknownProtocol", listener: (socket: tls.TLSSocket) => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener( + event: "checkContinue", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependOnceListener( + event: "request", + listener: (request: InstanceType, response: InstanceType) => void, + ): this; + prependOnceListener( + event: "session", + listener: (session: ServerHttp2Session) => void, + ): this; + prependOnceListener(event: "sessionError", listener: (err: Error) => void): this; + prependOnceListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): this; + prependOnceListener(event: "timeout", listener: () => void): this; + prependOnceListener(event: "unknownProtocol", listener: (socket: tls.TLSSocket) => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + /** + * A `Http2ServerRequest` object is created by {@link Server} or {@link SecureServer} and passed as the first argument to the `'request'` event. It may be used to access a request status, + * headers, and + * data. + * @since v8.4.0 + */ + export class Http2ServerRequest extends stream.Readable { + constructor( + stream: ServerHttp2Stream, + headers: IncomingHttpHeaders, + options: stream.ReadableOptions, + rawHeaders: readonly string[], + ); + /** + * The `request.aborted` property will be `true` if the request has + * been aborted. + * @since v10.1.0 + */ + readonly aborted: boolean; + /** + * The request authority pseudo header field. Because HTTP/2 allows requests + * to set either `:authority` or `host`, this value is derived from `req.headers[':authority']` if present. Otherwise, it is derived from `req.headers['host']`. + * @since v8.4.0 + */ + readonly authority: string; + /** + * See `request.socket`. + * @since v8.4.0 + * @deprecated Since v13.0.0 - Use `socket`. + */ + readonly connection: net.Socket | tls.TLSSocket; + /** + * The `request.complete` property will be `true` if the request has + * been completed, aborted, or destroyed. + * @since v12.10.0 + */ + readonly complete: boolean; + /** + * The request/response headers object. + * + * Key-value pairs of header names and values. Header names are lower-cased. + * + * ```js + * // Prints something like: + * // + * // { 'user-agent': 'curl/7.22.0', + * // host: '127.0.0.1:8000', + * // accept: '*' } + * console.log(request.headers); + * ``` + * + * See `HTTP/2 Headers Object`. + * + * In HTTP/2, the request path, host name, protocol, and method are represented as + * special headers prefixed with the `:` character (e.g. `':path'`). These special + * headers will be included in the `request.headers` object. Care must be taken not + * to inadvertently modify these special headers or errors may occur. For instance, + * removing all headers from the request will cause errors to occur: + * + * ```js + * removeAllHeaders(request.headers); + * assert(request.url); // Fails because the :path header has been removed + * ``` + * @since v8.4.0 + */ + readonly headers: IncomingHttpHeaders; + /** + * In case of server request, the HTTP version sent by the client. In the case of + * client response, the HTTP version of the connected-to server. Returns `'2.0'`. + * + * Also `message.httpVersionMajor` is the first integer and `message.httpVersionMinor` is the second. + * @since v8.4.0 + */ + readonly httpVersion: string; + readonly httpVersionMinor: number; + readonly httpVersionMajor: number; + /** + * The request method as a string. Read-only. Examples: `'GET'`, `'DELETE'`. + * @since v8.4.0 + */ + readonly method: string; + /** + * The raw request/response headers list exactly as they were received. + * + * The keys and values are in the same list. It is _not_ a + * list of tuples. So, the even-numbered offsets are key values, and the + * odd-numbered offsets are the associated values. + * + * Header names are not lowercased, and duplicates are not merged. + * + * ```js + * // Prints something like: + * // + * // [ 'user-agent', + * // 'this is invalid because there can be only one', + * // 'User-Agent', + * // 'curl/7.22.0', + * // 'Host', + * // '127.0.0.1:8000', + * // 'ACCEPT', + * // '*' ] + * console.log(request.rawHeaders); + * ``` + * @since v8.4.0 + */ + readonly rawHeaders: string[]; + /** + * The raw request/response trailer keys and values exactly as they were + * received. Only populated at the `'end'` event. + * @since v8.4.0 + */ + readonly rawTrailers: string[]; + /** + * The request scheme pseudo header field indicating the scheme + * portion of the target URL. + * @since v8.4.0 + */ + readonly scheme: string; + /** + * Returns a `Proxy` object that acts as a `net.Socket` (or `tls.TLSSocket`) but + * applies getters, setters, and methods based on HTTP/2 logic. + * + * `destroyed`, `readable`, and `writable` properties will be retrieved from and + * set on `request.stream`. + * + * `destroy`, `emit`, `end`, `on` and `once` methods will be called on `request.stream`. + * + * `setTimeout` method will be called on `request.stream.session`. + * + * `pause`, `read`, `resume`, and `write` will throw an error with code `ERR_HTTP2_NO_SOCKET_MANIPULATION`. See `Http2Session and Sockets` for + * more information. + * + * All other interactions will be routed directly to the socket. With TLS support, + * use `request.socket.getPeerCertificate()` to obtain the client's + * authentication details. + * @since v8.4.0 + */ + readonly socket: net.Socket | tls.TLSSocket; + /** + * The `Http2Stream` object backing the request. + * @since v8.4.0 + */ + readonly stream: ServerHttp2Stream; + /** + * The request/response trailers object. Only populated at the `'end'` event. + * @since v8.4.0 + */ + readonly trailers: IncomingHttpHeaders; + /** + * Request URL string. This contains only the URL that is present in the actual + * HTTP request. If the request is: + * + * ```http + * GET /status?name=ryan HTTP/1.1 + * Accept: text/plain + * ``` + * + * Then `request.url` will be: + * + * ```js + * '/status?name=ryan' + * ``` + * + * To parse the url into its parts, `new URL()` can be used: + * + * ```console + * $ node + * > new URL('/status?name=ryan', 'http://example.com') + * URL { + * href: 'http://example.com/status?name=ryan', + * origin: 'http://example.com', + * protocol: 'http:', + * username: '', + * password: '', + * host: 'example.com', + * hostname: 'example.com', + * port: '', + * pathname: '/status', + * search: '?name=ryan', + * searchParams: URLSearchParams { 'name' => 'ryan' }, + * hash: '' + * } + * ``` + * @since v8.4.0 + */ + url: string; + /** + * Sets the `Http2Stream`'s timeout value to `msecs`. If a callback is + * provided, then it is added as a listener on the `'timeout'` event on + * the response object. + * + * If no `'timeout'` listener is added to the request, the response, or + * the server, then `Http2Stream`s are destroyed when they time out. If a + * handler is assigned to the request, the response, or the server's `'timeout'`events, timed out sockets must be handled explicitly. + * @since v8.4.0 + */ + setTimeout(msecs: number, callback?: () => void): void; + read(size?: number): NonSharedBuffer | string | null; + addListener(event: "aborted", listener: (hadError: boolean, code: number) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + addListener(event: "end", listener: () => void): this; + addListener(event: "readable", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "aborted", hadError: boolean, code: number): boolean; + emit(event: "close"): boolean; + emit(event: "data", chunk: NonSharedBuffer | string): boolean; + emit(event: "end"): boolean; + emit(event: "readable"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "aborted", listener: (hadError: boolean, code: number) => void): this; + on(event: "close", listener: () => void): this; + on(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + on(event: "end", listener: () => void): this; + on(event: "readable", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "aborted", listener: (hadError: boolean, code: number) => void): this; + once(event: "close", listener: () => void): this; + once(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + once(event: "end", listener: () => void): this; + once(event: "readable", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "aborted", listener: (hadError: boolean, code: number) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + prependListener(event: "end", listener: () => void): this; + prependListener(event: "readable", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "aborted", listener: (hadError: boolean, code: number) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "data", listener: (chunk: NonSharedBuffer | string) => void): this; + prependOnceListener(event: "end", listener: () => void): this; + prependOnceListener(event: "readable", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + /** + * This object is created internally by an HTTP server, not by the user. It is + * passed as the second parameter to the `'request'` event. + * @since v8.4.0 + */ + export class Http2ServerResponse extends stream.Writable { + constructor(stream: ServerHttp2Stream); + /** + * See `response.socket`. + * @since v8.4.0 + * @deprecated Since v13.0.0 - Use `socket`. + */ + readonly connection: net.Socket | tls.TLSSocket; + /** + * Append a single header value to the header object. + * + * If the value is an array, this is equivalent to calling this method multiple times. + * + * If there were no previous values for the header, this is equivalent to calling {@link setHeader}. + * + * Attempting to set a header field name or value that contains invalid characters will result in a + * [TypeError](https://nodejs.org/docs/latest-v22.x/api/errors.html#class-typeerror) being thrown. + * + * ```js + * // Returns headers including "set-cookie: a" and "set-cookie: b" + * const server = http2.createServer((req, res) => { + * res.setHeader('set-cookie', 'a'); + * res.appendHeader('set-cookie', 'b'); + * res.writeHead(200); + * res.end('ok'); + * }); + * ``` + * @since v20.12.0 + */ + appendHeader(name: string, value: string | string[]): void; + /** + * Boolean value that indicates whether the response has completed. Starts + * as `false`. After `response.end()` executes, the value will be `true`. + * @since v8.4.0 + * @deprecated Since v13.4.0,v12.16.0 - Use `writableEnded`. + */ + readonly finished: boolean; + /** + * True if headers were sent, false otherwise (read-only). + * @since v8.4.0 + */ + readonly headersSent: boolean; + /** + * A reference to the original HTTP2 `request` object. + * @since v15.7.0 + */ + readonly req: Request; + /** + * Returns a `Proxy` object that acts as a `net.Socket` (or `tls.TLSSocket`) but + * applies getters, setters, and methods based on HTTP/2 logic. + * + * `destroyed`, `readable`, and `writable` properties will be retrieved from and + * set on `response.stream`. + * + * `destroy`, `emit`, `end`, `on` and `once` methods will be called on `response.stream`. + * + * `setTimeout` method will be called on `response.stream.session`. + * + * `pause`, `read`, `resume`, and `write` will throw an error with code `ERR_HTTP2_NO_SOCKET_MANIPULATION`. See `Http2Session and Sockets` for + * more information. + * + * All other interactions will be routed directly to the socket. + * + * ```js + * import http2 from 'node:http2'; + * const server = http2.createServer((req, res) => { + * const ip = req.socket.remoteAddress; + * const port = req.socket.remotePort; + * res.end(`Your IP address is ${ip} and your source port is ${port}.`); + * }).listen(3000); + * ``` + * @since v8.4.0 + */ + readonly socket: net.Socket | tls.TLSSocket; + /** + * The `Http2Stream` object backing the response. + * @since v8.4.0 + */ + readonly stream: ServerHttp2Stream; + /** + * When true, the Date header will be automatically generated and sent in + * the response if it is not already present in the headers. Defaults to true. + * + * This should only be disabled for testing; HTTP requires the Date header + * in responses. + * @since v8.4.0 + */ + sendDate: boolean; + /** + * When using implicit headers (not calling `response.writeHead()` explicitly), + * this property controls the status code that will be sent to the client when + * the headers get flushed. + * + * ```js + * response.statusCode = 404; + * ``` + * + * After response header was sent to the client, this property indicates the + * status code which was sent out. + * @since v8.4.0 + */ + statusCode: number; + /** + * Status message is not supported by HTTP/2 (RFC 7540 8.1.2.4). It returns + * an empty string. + * @since v8.4.0 + */ + statusMessage: ""; + /** + * This method adds HTTP trailing headers (a header but at the end of the + * message) to the response. + * + * Attempting to set a header field name or value that contains invalid characters + * will result in a `TypeError` being thrown. + * @since v8.4.0 + */ + addTrailers(trailers: OutgoingHttpHeaders): void; + /** + * This method signals to the server that all of the response headers and body + * have been sent; that server should consider this message complete. + * The method, `response.end()`, MUST be called on each response. + * + * If `data` is specified, it is equivalent to calling `response.write(data, encoding)` followed by `response.end(callback)`. + * + * If `callback` is specified, it will be called when the response stream + * is finished. + * @since v8.4.0 + */ + end(callback?: () => void): this; + end(data: string | Uint8Array, callback?: () => void): this; + end(data: string | Uint8Array, encoding: BufferEncoding, callback?: () => void): this; + /** + * Reads out a header that has already been queued but not sent to the client. + * The name is case-insensitive. + * + * ```js + * const contentType = response.getHeader('content-type'); + * ``` + * @since v8.4.0 + */ + getHeader(name: string): string; + /** + * Returns an array containing the unique names of the current outgoing headers. + * All header names are lowercase. + * + * ```js + * response.setHeader('Foo', 'bar'); + * response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + * + * const headerNames = response.getHeaderNames(); + * // headerNames === ['foo', 'set-cookie'] + * ``` + * @since v8.4.0 + */ + getHeaderNames(): string[]; + /** + * Returns a shallow copy of the current outgoing headers. Since a shallow copy + * is used, array values may be mutated without additional calls to various + * header-related http module methods. The keys of the returned object are the + * header names and the values are the respective header values. All header names + * are lowercase. + * + * The object returned by the `response.getHeaders()` method _does not_ prototypically inherit from the JavaScript `Object`. This means that typical `Object` methods such as `obj.toString()`, + * `obj.hasOwnProperty()`, and others + * are not defined and _will not work_. + * + * ```js + * response.setHeader('Foo', 'bar'); + * response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + * + * const headers = response.getHeaders(); + * // headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] } + * ``` + * @since v8.4.0 + */ + getHeaders(): OutgoingHttpHeaders; + /** + * Returns `true` if the header identified by `name` is currently set in the + * outgoing headers. The header name matching is case-insensitive. + * + * ```js + * const hasContentType = response.hasHeader('content-type'); + * ``` + * @since v8.4.0 + */ + hasHeader(name: string): boolean; + /** + * Removes a header that has been queued for implicit sending. + * + * ```js + * response.removeHeader('Content-Encoding'); + * ``` + * @since v8.4.0 + */ + removeHeader(name: string): void; + /** + * Sets a single header value for implicit headers. If this header already exists + * in the to-be-sent headers, its value will be replaced. Use an array of strings + * here to send multiple headers with the same name. + * + * ```js + * response.setHeader('Content-Type', 'text/html; charset=utf-8'); + * ``` + * + * or + * + * ```js + * response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']); + * ``` + * + * Attempting to set a header field name or value that contains invalid characters + * will result in a `TypeError` being thrown. + * + * When headers have been set with `response.setHeader()`, they will be merged + * with any headers passed to `response.writeHead()`, with the headers passed + * to `response.writeHead()` given precedence. + * + * ```js + * // Returns content-type = text/plain + * const server = http2.createServer((req, res) => { + * res.setHeader('Content-Type', 'text/html; charset=utf-8'); + * res.setHeader('X-Foo', 'bar'); + * res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); + * res.end('ok'); + * }); + * ``` + * @since v8.4.0 + */ + setHeader(name: string, value: number | string | readonly string[]): void; + /** + * Sets the `Http2Stream`'s timeout value to `msecs`. If a callback is + * provided, then it is added as a listener on the `'timeout'` event on + * the response object. + * + * If no `'timeout'` listener is added to the request, the response, or + * the server, then `Http2Stream` s are destroyed when they time out. If a + * handler is assigned to the request, the response, or the server's `'timeout'` events, timed out sockets must be handled explicitly. + * @since v8.4.0 + */ + setTimeout(msecs: number, callback?: () => void): void; + /** + * If this method is called and `response.writeHead()` has not been called, + * it will switch to implicit header mode and flush the implicit headers. + * + * This sends a chunk of the response body. This method may + * be called multiple times to provide successive parts of the body. + * + * In the `node:http` module, the response body is omitted when the + * request is a HEAD request. Similarly, the `204` and `304` responses _must not_ include a message body. + * + * `chunk` can be a string or a buffer. If `chunk` is a string, + * the second parameter specifies how to encode it into a byte stream. + * By default the `encoding` is `'utf8'`. `callback` will be called when this chunk + * of data is flushed. + * + * This is the raw HTTP body and has nothing to do with higher-level multi-part + * body encodings that may be used. + * + * The first time `response.write()` is called, it will send the buffered + * header information and the first chunk of the body to the client. The second + * time `response.write()` is called, Node.js assumes data will be streamed, + * and sends the new data separately. That is, the response is buffered up to the + * first chunk of the body. + * + * Returns `true` if the entire data was flushed successfully to the kernel + * buffer. Returns `false` if all or part of the data was queued in user memory.`'drain'` will be emitted when the buffer is free again. + * @since v8.4.0 + */ + write(chunk: string | Uint8Array, callback?: (err: Error) => void): boolean; + write(chunk: string | Uint8Array, encoding: BufferEncoding, callback?: (err: Error) => void): boolean; + /** + * Sends a status `100 Continue` to the client, indicating that the request body + * should be sent. See the `'checkContinue'` event on `Http2Server` and `Http2SecureServer`. + * @since v8.4.0 + */ + writeContinue(): void; + /** + * Sends a status `103 Early Hints` to the client with a Link header, + * indicating that the user agent can preload/preconnect the linked resources. + * The `hints` is an object containing the values of headers to be sent with + * early hints message. + * + * **Example** + * + * ```js + * const earlyHintsLink = '; rel=preload; as=style'; + * response.writeEarlyHints({ + * 'link': earlyHintsLink, + * }); + * + * const earlyHintsLinks = [ + * '; rel=preload; as=style', + * '; rel=preload; as=script', + * ]; + * response.writeEarlyHints({ + * 'link': earlyHintsLinks, + * }); + * ``` + * @since v18.11.0 + */ + writeEarlyHints(hints: Record): void; + /** + * Sends a response header to the request. The status code is a 3-digit HTTP + * status code, like `404`. The last argument, `headers`, are the response headers. + * + * Returns a reference to the `Http2ServerResponse`, so that calls can be chained. + * + * For compatibility with `HTTP/1`, a human-readable `statusMessage` may be + * passed as the second argument. However, because the `statusMessage` has no + * meaning within HTTP/2, the argument will have no effect and a process warning + * will be emitted. + * + * ```js + * const body = 'hello world'; + * response.writeHead(200, { + * 'Content-Length': Buffer.byteLength(body), + * 'Content-Type': 'text/plain; charset=utf-8', + * }); + * ``` + * + * `Content-Length` is given in bytes not characters. The`Buffer.byteLength()` API may be used to determine the number of bytes in a + * given encoding. On outbound messages, Node.js does not check if Content-Length + * and the length of the body being transmitted are equal or not. However, when + * receiving messages, Node.js will automatically reject messages when the `Content-Length` does not match the actual payload size. + * + * This method may be called at most one time on a message before `response.end()` is called. + * + * If `response.write()` or `response.end()` are called before calling + * this, the implicit/mutable headers will be calculated and call this function. + * + * When headers have been set with `response.setHeader()`, they will be merged + * with any headers passed to `response.writeHead()`, with the headers passed + * to `response.writeHead()` given precedence. + * + * ```js + * // Returns content-type = text/plain + * const server = http2.createServer((req, res) => { + * res.setHeader('Content-Type', 'text/html; charset=utf-8'); + * res.setHeader('X-Foo', 'bar'); + * res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); + * res.end('ok'); + * }); + * ``` + * + * Attempting to set a header field name or value that contains invalid characters + * will result in a `TypeError` being thrown. + * @since v8.4.0 + */ + writeHead(statusCode: number, headers?: OutgoingHttpHeaders): this; + writeHead(statusCode: number, statusMessage: string, headers?: OutgoingHttpHeaders): this; + /** + * Call `http2stream.pushStream()` with the given headers, and wrap the + * given `Http2Stream` on a newly created `Http2ServerResponse` as the callback + * parameter if successful. When `Http2ServerRequest` is closed, the callback is + * called with an error `ERR_HTTP2_INVALID_STREAM`. + * @since v8.4.0 + * @param headers An object describing the headers + * @param callback Called once `http2stream.pushStream()` is finished, or either when the attempt to create the pushed `Http2Stream` has failed or has been rejected, or the state of + * `Http2ServerRequest` is closed prior to calling the `http2stream.pushStream()` method + */ + createPushResponse( + headers: OutgoingHttpHeaders, + callback: (err: Error | null, res: Http2ServerResponse) => void, + ): void; + addListener(event: "close", listener: () => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "error", listener: (error: Error) => void): this; + addListener(event: "finish", listener: () => void): this; + addListener(event: "pipe", listener: (src: stream.Readable) => void): this; + addListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "close"): boolean; + emit(event: "drain"): boolean; + emit(event: "error", error: Error): boolean; + emit(event: "finish"): boolean; + emit(event: "pipe", src: stream.Readable): boolean; + emit(event: "unpipe", src: stream.Readable): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "close", listener: () => void): this; + on(event: "drain", listener: () => void): this; + on(event: "error", listener: (error: Error) => void): this; + on(event: "finish", listener: () => void): this; + on(event: "pipe", listener: (src: stream.Readable) => void): this; + on(event: "unpipe", listener: (src: stream.Readable) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "drain", listener: () => void): this; + once(event: "error", listener: (error: Error) => void): this; + once(event: "finish", listener: () => void): this; + once(event: "pipe", listener: (src: stream.Readable) => void): this; + once(event: "unpipe", listener: (src: stream.Readable) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "error", listener: (error: Error) => void): this; + prependListener(event: "finish", listener: () => void): this; + prependListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "error", listener: (error: Error) => void): this; + prependOnceListener(event: "finish", listener: () => void): this; + prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + export namespace constants { + const NGHTTP2_SESSION_SERVER: number; + const NGHTTP2_SESSION_CLIENT: number; + const NGHTTP2_STREAM_STATE_IDLE: number; + const NGHTTP2_STREAM_STATE_OPEN: number; + const NGHTTP2_STREAM_STATE_RESERVED_LOCAL: number; + const NGHTTP2_STREAM_STATE_RESERVED_REMOTE: number; + const NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: number; + const NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: number; + const NGHTTP2_STREAM_STATE_CLOSED: number; + const NGHTTP2_NO_ERROR: number; + const NGHTTP2_PROTOCOL_ERROR: number; + const NGHTTP2_INTERNAL_ERROR: number; + const NGHTTP2_FLOW_CONTROL_ERROR: number; + const NGHTTP2_SETTINGS_TIMEOUT: number; + const NGHTTP2_STREAM_CLOSED: number; + const NGHTTP2_FRAME_SIZE_ERROR: number; + const NGHTTP2_REFUSED_STREAM: number; + const NGHTTP2_CANCEL: number; + const NGHTTP2_COMPRESSION_ERROR: number; + const NGHTTP2_CONNECT_ERROR: number; + const NGHTTP2_ENHANCE_YOUR_CALM: number; + const NGHTTP2_INADEQUATE_SECURITY: number; + const NGHTTP2_HTTP_1_1_REQUIRED: number; + const NGHTTP2_ERR_FRAME_SIZE_ERROR: number; + const NGHTTP2_FLAG_NONE: number; + const NGHTTP2_FLAG_END_STREAM: number; + const NGHTTP2_FLAG_END_HEADERS: number; + const NGHTTP2_FLAG_ACK: number; + const NGHTTP2_FLAG_PADDED: number; + const NGHTTP2_FLAG_PRIORITY: number; + const DEFAULT_SETTINGS_HEADER_TABLE_SIZE: number; + const DEFAULT_SETTINGS_ENABLE_PUSH: number; + const DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: number; + const DEFAULT_SETTINGS_MAX_FRAME_SIZE: number; + const MAX_MAX_FRAME_SIZE: number; + const MIN_MAX_FRAME_SIZE: number; + const MAX_INITIAL_WINDOW_SIZE: number; + const NGHTTP2_DEFAULT_WEIGHT: number; + const NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: number; + const NGHTTP2_SETTINGS_ENABLE_PUSH: number; + const NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: number; + const NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: number; + const NGHTTP2_SETTINGS_MAX_FRAME_SIZE: number; + const NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: number; + const PADDING_STRATEGY_NONE: number; + const PADDING_STRATEGY_MAX: number; + const PADDING_STRATEGY_CALLBACK: number; + const HTTP2_HEADER_STATUS: string; + const HTTP2_HEADER_METHOD: string; + const HTTP2_HEADER_AUTHORITY: string; + const HTTP2_HEADER_SCHEME: string; + const HTTP2_HEADER_PATH: string; + const HTTP2_HEADER_ACCEPT_CHARSET: string; + const HTTP2_HEADER_ACCEPT_ENCODING: string; + const HTTP2_HEADER_ACCEPT_LANGUAGE: string; + const HTTP2_HEADER_ACCEPT_RANGES: string; + const HTTP2_HEADER_ACCEPT: string; + const HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS: string; + const HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS: string; + const HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS: string; + const HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: string; + const HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS: string; + const HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS: string; + const HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD: string; + const HTTP2_HEADER_AGE: string; + const HTTP2_HEADER_ALLOW: string; + const HTTP2_HEADER_AUTHORIZATION: string; + const HTTP2_HEADER_CACHE_CONTROL: string; + const HTTP2_HEADER_CONNECTION: string; + const HTTP2_HEADER_CONTENT_DISPOSITION: string; + const HTTP2_HEADER_CONTENT_ENCODING: string; + const HTTP2_HEADER_CONTENT_LANGUAGE: string; + const HTTP2_HEADER_CONTENT_LENGTH: string; + const HTTP2_HEADER_CONTENT_LOCATION: string; + const HTTP2_HEADER_CONTENT_MD5: string; + const HTTP2_HEADER_CONTENT_RANGE: string; + const HTTP2_HEADER_CONTENT_TYPE: string; + const HTTP2_HEADER_COOKIE: string; + const HTTP2_HEADER_DATE: string; + const HTTP2_HEADER_ETAG: string; + const HTTP2_HEADER_EXPECT: string; + const HTTP2_HEADER_EXPIRES: string; + const HTTP2_HEADER_FROM: string; + const HTTP2_HEADER_HOST: string; + const HTTP2_HEADER_IF_MATCH: string; + const HTTP2_HEADER_IF_MODIFIED_SINCE: string; + const HTTP2_HEADER_IF_NONE_MATCH: string; + const HTTP2_HEADER_IF_RANGE: string; + const HTTP2_HEADER_IF_UNMODIFIED_SINCE: string; + const HTTP2_HEADER_LAST_MODIFIED: string; + const HTTP2_HEADER_LINK: string; + const HTTP2_HEADER_LOCATION: string; + const HTTP2_HEADER_MAX_FORWARDS: string; + const HTTP2_HEADER_PREFER: string; + const HTTP2_HEADER_PROXY_AUTHENTICATE: string; + const HTTP2_HEADER_PROXY_AUTHORIZATION: string; + const HTTP2_HEADER_RANGE: string; + const HTTP2_HEADER_REFERER: string; + const HTTP2_HEADER_REFRESH: string; + const HTTP2_HEADER_RETRY_AFTER: string; + const HTTP2_HEADER_SERVER: string; + const HTTP2_HEADER_SET_COOKIE: string; + const HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: string; + const HTTP2_HEADER_TRANSFER_ENCODING: string; + const HTTP2_HEADER_TE: string; + const HTTP2_HEADER_UPGRADE: string; + const HTTP2_HEADER_USER_AGENT: string; + const HTTP2_HEADER_VARY: string; + const HTTP2_HEADER_VIA: string; + const HTTP2_HEADER_WWW_AUTHENTICATE: string; + const HTTP2_HEADER_HTTP2_SETTINGS: string; + const HTTP2_HEADER_KEEP_ALIVE: string; + const HTTP2_HEADER_PROXY_CONNECTION: string; + const HTTP2_METHOD_ACL: string; + const HTTP2_METHOD_BASELINE_CONTROL: string; + const HTTP2_METHOD_BIND: string; + const HTTP2_METHOD_CHECKIN: string; + const HTTP2_METHOD_CHECKOUT: string; + const HTTP2_METHOD_CONNECT: string; + const HTTP2_METHOD_COPY: string; + const HTTP2_METHOD_DELETE: string; + const HTTP2_METHOD_GET: string; + const HTTP2_METHOD_HEAD: string; + const HTTP2_METHOD_LABEL: string; + const HTTP2_METHOD_LINK: string; + const HTTP2_METHOD_LOCK: string; + const HTTP2_METHOD_MERGE: string; + const HTTP2_METHOD_MKACTIVITY: string; + const HTTP2_METHOD_MKCALENDAR: string; + const HTTP2_METHOD_MKCOL: string; + const HTTP2_METHOD_MKREDIRECTREF: string; + const HTTP2_METHOD_MKWORKSPACE: string; + const HTTP2_METHOD_MOVE: string; + const HTTP2_METHOD_OPTIONS: string; + const HTTP2_METHOD_ORDERPATCH: string; + const HTTP2_METHOD_PATCH: string; + const HTTP2_METHOD_POST: string; + const HTTP2_METHOD_PRI: string; + const HTTP2_METHOD_PROPFIND: string; + const HTTP2_METHOD_PROPPATCH: string; + const HTTP2_METHOD_PUT: string; + const HTTP2_METHOD_REBIND: string; + const HTTP2_METHOD_REPORT: string; + const HTTP2_METHOD_SEARCH: string; + const HTTP2_METHOD_TRACE: string; + const HTTP2_METHOD_UNBIND: string; + const HTTP2_METHOD_UNCHECKOUT: string; + const HTTP2_METHOD_UNLINK: string; + const HTTP2_METHOD_UNLOCK: string; + const HTTP2_METHOD_UPDATE: string; + const HTTP2_METHOD_UPDATEREDIRECTREF: string; + const HTTP2_METHOD_VERSION_CONTROL: string; + const HTTP_STATUS_CONTINUE: number; + const HTTP_STATUS_SWITCHING_PROTOCOLS: number; + const HTTP_STATUS_PROCESSING: number; + const HTTP_STATUS_OK: number; + const HTTP_STATUS_CREATED: number; + const HTTP_STATUS_ACCEPTED: number; + const HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION: number; + const HTTP_STATUS_NO_CONTENT: number; + const HTTP_STATUS_RESET_CONTENT: number; + const HTTP_STATUS_PARTIAL_CONTENT: number; + const HTTP_STATUS_MULTI_STATUS: number; + const HTTP_STATUS_ALREADY_REPORTED: number; + const HTTP_STATUS_IM_USED: number; + const HTTP_STATUS_MULTIPLE_CHOICES: number; + const HTTP_STATUS_MOVED_PERMANENTLY: number; + const HTTP_STATUS_FOUND: number; + const HTTP_STATUS_SEE_OTHER: number; + const HTTP_STATUS_NOT_MODIFIED: number; + const HTTP_STATUS_USE_PROXY: number; + const HTTP_STATUS_TEMPORARY_REDIRECT: number; + const HTTP_STATUS_PERMANENT_REDIRECT: number; + const HTTP_STATUS_BAD_REQUEST: number; + const HTTP_STATUS_UNAUTHORIZED: number; + const HTTP_STATUS_PAYMENT_REQUIRED: number; + const HTTP_STATUS_FORBIDDEN: number; + const HTTP_STATUS_NOT_FOUND: number; + const HTTP_STATUS_METHOD_NOT_ALLOWED: number; + const HTTP_STATUS_NOT_ACCEPTABLE: number; + const HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED: number; + const HTTP_STATUS_REQUEST_TIMEOUT: number; + const HTTP_STATUS_CONFLICT: number; + const HTTP_STATUS_GONE: number; + const HTTP_STATUS_LENGTH_REQUIRED: number; + const HTTP_STATUS_PRECONDITION_FAILED: number; + const HTTP_STATUS_PAYLOAD_TOO_LARGE: number; + const HTTP_STATUS_URI_TOO_LONG: number; + const HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: number; + const HTTP_STATUS_RANGE_NOT_SATISFIABLE: number; + const HTTP_STATUS_EXPECTATION_FAILED: number; + const HTTP_STATUS_TEAPOT: number; + const HTTP_STATUS_MISDIRECTED_REQUEST: number; + const HTTP_STATUS_UNPROCESSABLE_ENTITY: number; + const HTTP_STATUS_LOCKED: number; + const HTTP_STATUS_FAILED_DEPENDENCY: number; + const HTTP_STATUS_UNORDERED_COLLECTION: number; + const HTTP_STATUS_UPGRADE_REQUIRED: number; + const HTTP_STATUS_PRECONDITION_REQUIRED: number; + const HTTP_STATUS_TOO_MANY_REQUESTS: number; + const HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE: number; + const HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS: number; + const HTTP_STATUS_INTERNAL_SERVER_ERROR: number; + const HTTP_STATUS_NOT_IMPLEMENTED: number; + const HTTP_STATUS_BAD_GATEWAY: number; + const HTTP_STATUS_SERVICE_UNAVAILABLE: number; + const HTTP_STATUS_GATEWAY_TIMEOUT: number; + const HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED: number; + const HTTP_STATUS_VARIANT_ALSO_NEGOTIATES: number; + const HTTP_STATUS_INSUFFICIENT_STORAGE: number; + const HTTP_STATUS_LOOP_DETECTED: number; + const HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED: number; + const HTTP_STATUS_NOT_EXTENDED: number; + const HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED: number; + } + /** + * This symbol can be set as a property on the HTTP/2 headers object with + * an array value in order to provide a list of headers considered sensitive. + */ + export const sensitiveHeaders: symbol; + /** + * Returns an object containing the default settings for an `Http2Session` instance. This method returns a new object instance every time it is called + * so instances returned may be safely modified for use. + * @since v8.4.0 + */ + export function getDefaultSettings(): Settings; + /** + * Returns a `Buffer` instance containing serialized representation of the given + * HTTP/2 settings as specified in the [HTTP/2](https://tools.ietf.org/html/rfc7540) specification. This is intended + * for use with the `HTTP2-Settings` header field. + * + * ```js + * import http2 from 'node:http2'; + * + * const packed = http2.getPackedSettings({ enablePush: false }); + * + * console.log(packed.toString('base64')); + * // Prints: AAIAAAAA + * ``` + * @since v8.4.0 + */ + export function getPackedSettings(settings: Settings): NonSharedBuffer; + /** + * Returns a `HTTP/2 Settings Object` containing the deserialized settings from + * the given `Buffer` as generated by `http2.getPackedSettings()`. + * @since v8.4.0 + * @param buf The packed settings. + */ + export function getUnpackedSettings(buf: Uint8Array): Settings; + /** + * Returns a `net.Server` instance that creates and manages `Http2Session` instances. + * + * Since there are no browsers known that support [unencrypted HTTP/2](https://http2.github.io/faq/#does-http2-require-encryption), the use of {@link createSecureServer} is necessary when + * communicating + * with browser clients. + * + * ```js + * import http2 from 'node:http2'; + * + * // Create an unencrypted HTTP/2 server. + * // Since there are no browsers known that support + * // unencrypted HTTP/2, the use of `http2.createSecureServer()` + * // is necessary when communicating with browser clients. + * const server = http2.createServer(); + * + * server.on('stream', (stream, headers) => { + * stream.respond({ + * 'content-type': 'text/html; charset=utf-8', + * ':status': 200, + * }); + * stream.end('

Hello World

'); + * }); + * + * server.listen(8000); + * ``` + * @since v8.4.0 + * @param onRequestHandler See `Compatibility API` + */ + export function createServer( + onRequestHandler?: (request: Http2ServerRequest, response: Http2ServerResponse) => void, + ): Http2Server; + export function createServer< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + >( + options: ServerOptions, + onRequestHandler?: (request: InstanceType, response: InstanceType) => void, + ): Http2Server; + /** + * Returns a `tls.Server` instance that creates and manages `Http2Session` instances. + * + * ```js + * import http2 from 'node:http2'; + * import fs from 'node:fs'; + * + * const options = { + * key: fs.readFileSync('server-key.pem'), + * cert: fs.readFileSync('server-cert.pem'), + * }; + * + * // Create a secure HTTP/2 server + * const server = http2.createSecureServer(options); + * + * server.on('stream', (stream, headers) => { + * stream.respond({ + * 'content-type': 'text/html; charset=utf-8', + * ':status': 200, + * }); + * stream.end('

Hello World

'); + * }); + * + * server.listen(8443); + * ``` + * @since v8.4.0 + * @param onRequestHandler See `Compatibility API` + */ + export function createSecureServer( + onRequestHandler?: (request: Http2ServerRequest, response: Http2ServerResponse) => void, + ): Http2SecureServer; + export function createSecureServer< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + >( + options: SecureServerOptions, + onRequestHandler?: (request: InstanceType, response: InstanceType) => void, + ): Http2SecureServer; + /** + * Returns a `ClientHttp2Session` instance. + * + * ```js + * import http2 from 'node:http2'; + * const client = http2.connect('https://localhost:1234'); + * + * // Use the client + * + * client.close(); + * ``` + * @since v8.4.0 + * @param authority The remote HTTP/2 server to connect to. This must be in the form of a minimal, valid URL with the `http://` or `https://` prefix, host name, and IP port (if a non-default port + * is used). Userinfo (user ID and password), path, querystring, and fragment details in the URL will be ignored. + * @param listener Will be registered as a one-time listener of the {@link 'connect'} event. + */ + export function connect( + authority: string | url.URL, + listener: (session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket) => void, + ): ClientHttp2Session; + export function connect( + authority: string | url.URL, + options?: ClientSessionOptions | SecureClientSessionOptions, + listener?: (session: ClientHttp2Session, socket: net.Socket | tls.TLSSocket) => void, + ): ClientHttp2Session; + /** + * Create an HTTP/2 server session from an existing socket. + * @param socket A Duplex Stream + * @param options Any `{@link createServer}` options can be provided. + * @since v20.12.0 + */ + export function performServerHandshake< + Http1Request extends typeof IncomingMessage = typeof IncomingMessage, + Http1Response extends typeof ServerResponse> = typeof ServerResponse, + Http2Request extends typeof Http2ServerRequest = typeof Http2ServerRequest, + Http2Response extends typeof Http2ServerResponse> = typeof Http2ServerResponse, + >( + socket: stream.Duplex, + options?: ServerOptions, + ): ServerHttp2Session; +} +declare module "node:http2" { + export * from "http2"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/https.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/https.d.ts new file mode 100644 index 00000000..e0502558 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/https.d.ts @@ -0,0 +1,579 @@ +/** + * HTTPS is the HTTP protocol over TLS/SSL. In Node.js this is implemented as a + * separate module. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/https.js) + */ +declare module "https" { + import { NonSharedBuffer } from "node:buffer"; + import { Duplex } from "node:stream"; + import * as tls from "node:tls"; + import * as http from "node:http"; + import { URL } from "node:url"; + interface ServerOptions< + Request extends typeof http.IncomingMessage = typeof http.IncomingMessage, + Response extends typeof http.ServerResponse> = typeof http.ServerResponse, + > extends http.ServerOptions, tls.TlsOptions {} + interface RequestOptions extends http.RequestOptions, tls.SecureContextOptions { + checkServerIdentity?: + | ((hostname: string, cert: tls.DetailedPeerCertificate) => Error | undefined) + | undefined; + rejectUnauthorized?: boolean | undefined; // Defaults to true + servername?: string | undefined; // SNI TLS Extension + } + interface AgentOptions extends http.AgentOptions, tls.ConnectionOptions { + maxCachedSessions?: number | undefined; + } + /** + * An `Agent` object for HTTPS similar to `http.Agent`. See {@link request} for more information. + * @since v0.4.5 + */ + class Agent extends http.Agent { + constructor(options?: AgentOptions); + options: AgentOptions; + createConnection( + options: RequestOptions, + callback?: (err: Error | null, stream: Duplex) => void, + ): Duplex | null | undefined; + getName(options?: RequestOptions): string; + } + interface Server< + Request extends typeof http.IncomingMessage = typeof http.IncomingMessage, + Response extends typeof http.ServerResponse> = typeof http.ServerResponse, + > extends http.Server {} + /** + * See `http.Server` for more information. + * @since v0.3.4 + */ + class Server< + Request extends typeof http.IncomingMessage = typeof http.IncomingMessage, + Response extends typeof http.ServerResponse> = typeof http.ServerResponse, + > extends tls.Server { + constructor(requestListener?: http.RequestListener); + constructor( + options: ServerOptions, + requestListener?: http.RequestListener, + ); + /** + * Closes all connections connected to this server. + * @since v18.2.0 + */ + closeAllConnections(): void; + /** + * Closes all connections connected to this server which are not sending a request or waiting for a response. + * @since v18.2.0 + */ + closeIdleConnections(): void; + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: tls.TLSSocket) => void): this; + addListener( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + addListener( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + addListener( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + addListener(event: "secureConnection", listener: (tlsSocket: tls.TLSSocket) => void): this; + addListener(event: "tlsClientError", listener: (err: Error, tlsSocket: tls.TLSSocket) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "connection", listener: (socket: Duplex) => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "listening", listener: () => void): this; + addListener(event: "checkContinue", listener: http.RequestListener): this; + addListener(event: "checkExpectation", listener: http.RequestListener): this; + addListener(event: "clientError", listener: (err: Error, socket: Duplex) => void): this; + addListener( + event: "connect", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + addListener(event: "request", listener: http.RequestListener): this; + addListener( + event: "upgrade", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + emit(event: string, ...args: any[]): boolean; + emit(event: "keylog", line: NonSharedBuffer, tlsSocket: tls.TLSSocket): boolean; + emit( + event: "newSession", + sessionId: NonSharedBuffer, + sessionData: NonSharedBuffer, + callback: () => void, + ): boolean; + emit( + event: "OCSPRequest", + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ): boolean; + emit( + event: "resumeSession", + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ): boolean; + emit(event: "secureConnection", tlsSocket: tls.TLSSocket): boolean; + emit(event: "tlsClientError", err: Error, tlsSocket: tls.TLSSocket): boolean; + emit(event: "close"): boolean; + emit(event: "connection", socket: Duplex): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "listening"): boolean; + emit( + event: "checkContinue", + req: InstanceType, + res: InstanceType, + ): boolean; + emit( + event: "checkExpectation", + req: InstanceType, + res: InstanceType, + ): boolean; + emit(event: "clientError", err: Error, socket: Duplex): boolean; + emit(event: "connect", req: InstanceType, socket: Duplex, head: NonSharedBuffer): boolean; + emit( + event: "request", + req: InstanceType, + res: InstanceType, + ): boolean; + emit(event: "upgrade", req: InstanceType, socket: Duplex, head: NonSharedBuffer): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: tls.TLSSocket) => void): this; + on( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + on( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + on( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + on(event: "secureConnection", listener: (tlsSocket: tls.TLSSocket) => void): this; + on(event: "tlsClientError", listener: (err: Error, tlsSocket: tls.TLSSocket) => void): this; + on(event: "close", listener: () => void): this; + on(event: "connection", listener: (socket: Duplex) => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "listening", listener: () => void): this; + on(event: "checkContinue", listener: http.RequestListener): this; + on(event: "checkExpectation", listener: http.RequestListener): this; + on(event: "clientError", listener: (err: Error, socket: Duplex) => void): this; + on( + event: "connect", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + on(event: "request", listener: http.RequestListener): this; + on( + event: "upgrade", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: tls.TLSSocket) => void): this; + once( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + once( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + once( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + once(event: "secureConnection", listener: (tlsSocket: tls.TLSSocket) => void): this; + once(event: "tlsClientError", listener: (err: Error, tlsSocket: tls.TLSSocket) => void): this; + once(event: "close", listener: () => void): this; + once(event: "connection", listener: (socket: Duplex) => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "listening", listener: () => void): this; + once(event: "checkContinue", listener: http.RequestListener): this; + once(event: "checkExpectation", listener: http.RequestListener): this; + once(event: "clientError", listener: (err: Error, socket: Duplex) => void): this; + once( + event: "connect", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + once(event: "request", listener: http.RequestListener): this; + once( + event: "upgrade", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: tls.TLSSocket) => void): this; + prependListener( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + prependListener( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + prependListener( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + prependListener(event: "secureConnection", listener: (tlsSocket: tls.TLSSocket) => void): this; + prependListener(event: "tlsClientError", listener: (err: Error, tlsSocket: tls.TLSSocket) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "connection", listener: (socket: Duplex) => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "listening", listener: () => void): this; + prependListener(event: "checkContinue", listener: http.RequestListener): this; + prependListener(event: "checkExpectation", listener: http.RequestListener): this; + prependListener(event: "clientError", listener: (err: Error, socket: Duplex) => void): this; + prependListener( + event: "connect", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + prependListener(event: "request", listener: http.RequestListener): this; + prependListener( + event: "upgrade", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: tls.TLSSocket) => void): this; + prependOnceListener( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + prependOnceListener( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + prependOnceListener( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + prependOnceListener(event: "secureConnection", listener: (tlsSocket: tls.TLSSocket) => void): this; + prependOnceListener(event: "tlsClientError", listener: (err: Error, tlsSocket: tls.TLSSocket) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "connection", listener: (socket: Duplex) => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "listening", listener: () => void): this; + prependOnceListener(event: "checkContinue", listener: http.RequestListener): this; + prependOnceListener(event: "checkExpectation", listener: http.RequestListener): this; + prependOnceListener(event: "clientError", listener: (err: Error, socket: Duplex) => void): this; + prependOnceListener( + event: "connect", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + prependOnceListener(event: "request", listener: http.RequestListener): this; + prependOnceListener( + event: "upgrade", + listener: (req: InstanceType, socket: Duplex, head: NonSharedBuffer) => void, + ): this; + } + /** + * ```js + * // curl -k https://localhost:8000/ + * import https from 'node:https'; + * import fs from 'node:fs'; + * + * const options = { + * key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'), + * cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem'), + * }; + * + * https.createServer(options, (req, res) => { + * res.writeHead(200); + * res.end('hello world\n'); + * }).listen(8000); + * ``` + * + * Or + * + * ```js + * import https from 'node:https'; + * import fs from 'node:fs'; + * + * const options = { + * pfx: fs.readFileSync('test/fixtures/test_cert.pfx'), + * passphrase: 'sample', + * }; + * + * https.createServer(options, (req, res) => { + * res.writeHead(200); + * res.end('hello world\n'); + * }).listen(8000); + * ``` + * @since v0.3.4 + * @param options Accepts `options` from `createServer`, `createSecureContext` and `createServer`. + * @param requestListener A listener to be added to the `'request'` event. + */ + function createServer< + Request extends typeof http.IncomingMessage = typeof http.IncomingMessage, + Response extends typeof http.ServerResponse> = typeof http.ServerResponse, + >(requestListener?: http.RequestListener): Server; + function createServer< + Request extends typeof http.IncomingMessage = typeof http.IncomingMessage, + Response extends typeof http.ServerResponse> = typeof http.ServerResponse, + >( + options: ServerOptions, + requestListener?: http.RequestListener, + ): Server; + /** + * Makes a request to a secure web server. + * + * The following additional `options` from `tls.connect()` are also accepted: `ca`, `cert`, `ciphers`, `clientCertEngine`, `crl`, `dhparam`, `ecdhCurve`, `honorCipherOrder`, `key`, `passphrase`, + * `pfx`, `rejectUnauthorized`, `secureOptions`, `secureProtocol`, `servername`, `sessionIdContext`, `highWaterMark`. + * + * `options` can be an object, a string, or a `URL` object. If `options` is a + * string, it is automatically parsed with `new URL()`. If it is a `URL` object, it will be automatically converted to an ordinary `options` object. + * + * `https.request()` returns an instance of the `http.ClientRequest` class. The `ClientRequest` instance is a writable stream. If one needs to + * upload a file with a POST request, then write to the `ClientRequest` object. + * + * ```js + * import https from 'node:https'; + * + * const options = { + * hostname: 'encrypted.google.com', + * port: 443, + * path: '/', + * method: 'GET', + * }; + * + * const req = https.request(options, (res) => { + * console.log('statusCode:', res.statusCode); + * console.log('headers:', res.headers); + * + * res.on('data', (d) => { + * process.stdout.write(d); + * }); + * }); + * + * req.on('error', (e) => { + * console.error(e); + * }); + * req.end(); + * ``` + * + * Example using options from `tls.connect()`: + * + * ```js + * const options = { + * hostname: 'encrypted.google.com', + * port: 443, + * path: '/', + * method: 'GET', + * key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'), + * cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem'), + * }; + * options.agent = new https.Agent(options); + * + * const req = https.request(options, (res) => { + * // ... + * }); + * ``` + * + * Alternatively, opt out of connection pooling by not using an `Agent`. + * + * ```js + * const options = { + * hostname: 'encrypted.google.com', + * port: 443, + * path: '/', + * method: 'GET', + * key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'), + * cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem'), + * agent: false, + * }; + * + * const req = https.request(options, (res) => { + * // ... + * }); + * ``` + * + * Example using a `URL` as `options`: + * + * ```js + * const options = new URL('https://abc:xyz@example.com'); + * + * const req = https.request(options, (res) => { + * // ... + * }); + * ``` + * + * Example pinning on certificate fingerprint, or the public key (similar to`pin-sha256`): + * + * ```js + * import tls from 'node:tls'; + * import https from 'node:https'; + * import crypto from 'node:crypto'; + * + * function sha256(s) { + * return crypto.createHash('sha256').update(s).digest('base64'); + * } + * const options = { + * hostname: 'github.com', + * port: 443, + * path: '/', + * method: 'GET', + * checkServerIdentity: function(host, cert) { + * // Make sure the certificate is issued to the host we are connected to + * const err = tls.checkServerIdentity(host, cert); + * if (err) { + * return err; + * } + * + * // Pin the public key, similar to HPKP pin-sha256 pinning + * const pubkey256 = 'pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU='; + * if (sha256(cert.pubkey) !== pubkey256) { + * const msg = 'Certificate verification error: ' + + * `The public key of '${cert.subject.CN}' ` + + * 'does not match our pinned fingerprint'; + * return new Error(msg); + * } + * + * // Pin the exact certificate, rather than the pub key + * const cert256 = '25:FE:39:32:D9:63:8C:8A:FC:A1:9A:29:87:' + + * 'D8:3E:4C:1D:98:DB:71:E4:1A:48:03:98:EA:22:6A:BD:8B:93:16'; + * if (cert.fingerprint256 !== cert256) { + * const msg = 'Certificate verification error: ' + + * `The certificate of '${cert.subject.CN}' ` + + * 'does not match our pinned fingerprint'; + * return new Error(msg); + * } + * + * // This loop is informational only. + * // Print the certificate and public key fingerprints of all certs in the + * // chain. Its common to pin the public key of the issuer on the public + * // internet, while pinning the public key of the service in sensitive + * // environments. + * do { + * console.log('Subject Common Name:', cert.subject.CN); + * console.log(' Certificate SHA256 fingerprint:', cert.fingerprint256); + * + * hash = crypto.createHash('sha256'); + * console.log(' Public key ping-sha256:', sha256(cert.pubkey)); + * + * lastprint256 = cert.fingerprint256; + * cert = cert.issuerCertificate; + * } while (cert.fingerprint256 !== lastprint256); + * + * }, + * }; + * + * options.agent = new https.Agent(options); + * const req = https.request(options, (res) => { + * console.log('All OK. Server matched our pinned cert or public key'); + * console.log('statusCode:', res.statusCode); + * // Print the HPKP values + * console.log('headers:', res.headers['public-key-pins']); + * + * res.on('data', (d) => {}); + * }); + * + * req.on('error', (e) => { + * console.error(e.message); + * }); + * req.end(); + * ``` + * + * Outputs for example: + * + * ```text + * Subject Common Name: github.com + * Certificate SHA256 fingerprint: 25:FE:39:32:D9:63:8C:8A:FC:A1:9A:29:87:D8:3E:4C:1D:98:DB:71:E4:1A:48:03:98:EA:22:6A:BD:8B:93:16 + * Public key ping-sha256: pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU= + * Subject Common Name: DigiCert SHA2 Extended Validation Server CA + * Certificate SHA256 fingerprint: 40:3E:06:2A:26:53:05:91:13:28:5B:AF:80:A0:D4:AE:42:2C:84:8C:9F:78:FA:D0:1F:C9:4B:C5:B8:7F:EF:1A + * Public key ping-sha256: RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= + * Subject Common Name: DigiCert High Assurance EV Root CA + * Certificate SHA256 fingerprint: 74:31:E5:F4:C3:C1:CE:46:90:77:4F:0B:61:E0:54:40:88:3B:A9:A0:1E:D0:0B:A6:AB:D7:80:6E:D3:B1:18:CF + * Public key ping-sha256: WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18= + * All OK. Server matched our pinned cert or public key + * statusCode: 200 + * headers: max-age=0; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; pin-sha256="RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho="; + * pin-sha256="k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K+59sNQws="; pin-sha256="K87oWBWM9UZfyddvDfoxL+8lpNyoUB2ptGtn0fv6G2Q="; pin-sha256="IQBnNBEiFuhj+8x6X8XLgh01V9Ic5/V3IRQLNFFc7v4="; + * pin-sha256="iie1VXtL7HzAMF+/PVPR9xzT80kQxdZeJ+zduCB3uj0="; pin-sha256="LvRiGEjRqfzurezaWuj8Wie2gyHMrW5Q06LspMnox7A="; includeSubDomains + * ``` + * @since v0.3.6 + * @param options Accepts all `options` from `request`, with some differences in default values: + */ + function request( + options: RequestOptions | string | URL, + callback?: (res: http.IncomingMessage) => void, + ): http.ClientRequest; + function request( + url: string | URL, + options: RequestOptions, + callback?: (res: http.IncomingMessage) => void, + ): http.ClientRequest; + /** + * Like `http.get()` but for HTTPS. + * + * `options` can be an object, a string, or a `URL` object. If `options` is a + * string, it is automatically parsed with `new URL()`. If it is a `URL` object, it will be automatically converted to an ordinary `options` object. + * + * ```js + * import https from 'node:https'; + * + * https.get('https://encrypted.google.com/', (res) => { + * console.log('statusCode:', res.statusCode); + * console.log('headers:', res.headers); + * + * res.on('data', (d) => { + * process.stdout.write(d); + * }); + * + * }).on('error', (e) => { + * console.error(e); + * }); + * ``` + * @since v0.3.6 + * @param options Accepts the same `options` as {@link request}, with the `method` always set to `GET`. + */ + function get( + options: RequestOptions | string | URL, + callback?: (res: http.IncomingMessage) => void, + ): http.ClientRequest; + function get( + url: string | URL, + options: RequestOptions, + callback?: (res: http.IncomingMessage) => void, + ): http.ClientRequest; + let globalAgent: Agent; +} +declare module "node:https" { + export * from "https"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/index.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/index.d.ts new file mode 100644 index 00000000..c9edbd78 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/index.d.ts @@ -0,0 +1,97 @@ +/** + * License for programmatically and manually incorporated + * documentation aka. `JSDoc` from https://github.com/nodejs/node/tree/master/doc + * + * Copyright Node.js contributors. All rights reserved. + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +// NOTE: These definitions support Node.js and TypeScript 5.7+. + +// Reference required TypeScript libs: +/// + +// TypeScript backwards-compatibility definitions: +/// + +// Definitions specific to TypeScript 5.7+: +/// +/// + +// Definitions for Node.js modules that are not specific to any version of TypeScript: +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/inspector.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/inspector.d.ts new file mode 100644 index 00000000..1f1a6fef --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/inspector.d.ts @@ -0,0 +1,253 @@ +/** + * The `node:inspector` module provides an API for interacting with the V8 + * inspector. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/inspector.js) + */ +declare module "inspector" { + import EventEmitter = require("node:events"); + /** + * The `inspector.Session` is used for dispatching messages to the V8 inspector + * back-end and receiving message responses and notifications. + */ + class Session extends EventEmitter { + /** + * Create a new instance of the inspector.Session class. + * The inspector session needs to be connected through `session.connect()` before the messages can be dispatched to the inspector backend. + */ + constructor(); + /** + * Connects a session to the inspector back-end. + */ + connect(): void; + /** + * Connects a session to the inspector back-end. + * An exception will be thrown if this API was not called on a Worker thread. + * @since v12.11.0 + */ + connectToMainThread(): void; + /** + * Immediately close the session. All pending message callbacks will be called with an error. + * `session.connect()` will need to be called to be able to send messages again. + * Reconnected session will lose all inspector state, such as enabled agents or configured breakpoints. + */ + disconnect(): void; + } + /** + * Activate inspector on host and port. Equivalent to `node --inspect=[[host:]port]`, but can be done programmatically after node has + * started. + * + * If wait is `true`, will block until a client has connected to the inspect port + * and flow control has been passed to the debugger client. + * + * See the [security warning](https://nodejs.org/docs/latest-v22.x/api/cli.html#warning-binding-inspector-to-a-public-ipport-combination-is-insecure) + * regarding the `host` parameter usage. + * @param port Port to listen on for inspector connections. Defaults to what was specified on the CLI. + * @param host Host to listen on for inspector connections. Defaults to what was specified on the CLI. + * @param wait Block until a client has connected. Defaults to what was specified on the CLI. + * @returns Disposable that calls `inspector.close()`. + */ + function open(port?: number, host?: string, wait?: boolean): Disposable; + /** + * Deactivate the inspector. Blocks until there are no active connections. + */ + function close(): void; + /** + * Return the URL of the active inspector, or `undefined` if there is none. + * + * ```console + * $ node --inspect -p 'inspector.url()' + * Debugger listening on ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 + * For help, see: https://nodejs.org/en/docs/inspector + * ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 + * + * $ node --inspect=localhost:3000 -p 'inspector.url()' + * Debugger listening on ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a + * For help, see: https://nodejs.org/en/docs/inspector + * ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a + * + * $ node -p 'inspector.url()' + * undefined + * ``` + */ + function url(): string | undefined; + /** + * Blocks until a client (existing or connected later) has sent `Runtime.runIfWaitingForDebugger` command. + * + * An exception will be thrown if there is no active inspector. + * @since v12.7.0 + */ + function waitForDebugger(): void; + // These methods are exposed by the V8 inspector console API (inspector/v8-console.h). + // The method signatures differ from those of the Node.js console, and are deliberately + // typed permissively. + interface InspectorConsole { + debug(...data: any[]): void; + error(...data: any[]): void; + info(...data: any[]): void; + log(...data: any[]): void; + warn(...data: any[]): void; + dir(...data: any[]): void; + dirxml(...data: any[]): void; + table(...data: any[]): void; + trace(...data: any[]): void; + group(...data: any[]): void; + groupCollapsed(...data: any[]): void; + groupEnd(...data: any[]): void; + clear(...data: any[]): void; + count(label?: any): void; + countReset(label?: any): void; + assert(value?: any, ...data: any[]): void; + profile(label?: any): void; + profileEnd(label?: any): void; + time(label?: any): void; + timeLog(label?: any): void; + timeStamp(label?: any): void; + } + /** + * An object to send messages to the remote inspector console. + * @since v11.0.0 + */ + const console: InspectorConsole; + // DevTools protocol event broadcast methods + namespace Network { + /** + * This feature is only available with the `--experimental-network-inspection` flag enabled. + * + * Broadcasts the `Network.requestWillBeSent` event to connected frontends. This event indicates that + * the application is about to send an HTTP request. + * @since v22.6.0 + */ + function requestWillBeSent(params: RequestWillBeSentEventDataType): void; + /** + * This feature is only available with the `--experimental-network-inspection` flag enabled. + * + * Broadcasts the `Network.dataReceived` event to connected frontends, or buffers the data if + * `Network.streamResourceContent` command was not invoked for the given request yet. + * + * Also enables `Network.getResponseBody` command to retrieve the response data. + * @since v22.17.0 + */ + function dataReceived(params: DataReceivedEventDataType): void; + /** + * This feature is only available with the `--experimental-network-inspection` flag enabled. + * + * Enables `Network.getRequestPostData` command to retrieve the request data. + * @since v22.18.0 + */ + function dataSent(params: unknown): void; + /** + * This feature is only available with the `--experimental-network-inspection` flag enabled. + * + * Broadcasts the `Network.responseReceived` event to connected frontends. This event indicates that + * HTTP response is available. + * @since v22.6.0 + */ + function responseReceived(params: ResponseReceivedEventDataType): void; + /** + * This feature is only available with the `--experimental-network-inspection` flag enabled. + * + * Broadcasts the `Network.loadingFinished` event to connected frontends. This event indicates that + * HTTP request has finished loading. + * @since v22.6.0 + */ + function loadingFinished(params: LoadingFinishedEventDataType): void; + /** + * This feature is only available with the `--experimental-network-inspection` flag enabled. + * + * Broadcasts the `Network.loadingFailed` event to connected frontends. This event indicates that + * HTTP request has failed to load. + * @since v22.7.0 + */ + function loadingFailed(params: LoadingFailedEventDataType): void; + } + namespace NetworkResources { + /** + * This feature is only available with the `--experimental-inspector-network-resource` flag enabled. + * + * The inspector.NetworkResources.put method is used to provide a response for a loadNetworkResource + * request issued via the Chrome DevTools Protocol (CDP). + * This is typically triggered when a source map is specified by URL, and a DevTools frontend—such as + * Chrome—requests the resource to retrieve the source map. + * + * This method allows developers to predefine the resource content to be served in response to such CDP requests. + * + * ```js + * const inspector = require('node:inspector'); + * // By preemptively calling put to register the resource, a source map can be resolved when + * // a loadNetworkResource request is made from the frontend. + * async function setNetworkResources() { + * const mapUrl = 'http://localhost:3000/dist/app.js.map'; + * const tsUrl = 'http://localhost:3000/src/app.ts'; + * const distAppJsMap = await fetch(mapUrl).then((res) => res.text()); + * const srcAppTs = await fetch(tsUrl).then((res) => res.text()); + * inspector.NetworkResources.put(mapUrl, distAppJsMap); + * inspector.NetworkResources.put(tsUrl, srcAppTs); + * }; + * setNetworkResources().then(() => { + * require('./dist/app'); + * }); + * ``` + * + * For more details, see the official CDP documentation: [Network.loadNetworkResource](https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-loadNetworkResource) + * @since v22.19.0 + * @experimental + */ + function put(url: string, data: string): void; + } +} + +/** + * The `node:inspector` module provides an API for interacting with the V8 + * inspector. + */ +declare module "node:inspector" { + export * from "inspector"; +} + +/** + * The `node:inspector/promises` module provides an API for interacting with the V8 + * inspector. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/inspector/promises.js) + * @since v19.0.0 + */ +declare module "inspector/promises" { + import EventEmitter = require("node:events"); + export { close, console, NetworkResources, open, url, waitForDebugger } from "inspector"; + /** + * The `inspector.Session` is used for dispatching messages to the V8 inspector + * back-end and receiving message responses and notifications. + * @since v19.0.0 + */ + export class Session extends EventEmitter { + /** + * Create a new instance of the inspector.Session class. + * The inspector session needs to be connected through `session.connect()` before the messages can be dispatched to the inspector backend. + */ + constructor(); + /** + * Connects a session to the inspector back-end. + */ + connect(): void; + /** + * Connects a session to the inspector back-end. + * An exception will be thrown if this API was not called on a Worker thread. + * @since v12.11.0 + */ + connectToMainThread(): void; + /** + * Immediately close the session. All pending message callbacks will be called with an error. + * `session.connect()` will need to be called to be able to send messages again. + * Reconnected session will lose all inspector state, such as enabled agents or configured breakpoints. + */ + disconnect(): void; + } +} + +/** + * The `node:inspector/promises` module provides an API for interacting with the V8 + * inspector. + * @since v19.0.0 + */ +declare module "node:inspector/promises" { + export * from "inspector/promises"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/inspector.generated.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/inspector.generated.d.ts new file mode 100644 index 00000000..bcf0b3bb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/inspector.generated.d.ts @@ -0,0 +1,4052 @@ +// These definitions are automatically generated by the generate-inspector script. +// Do not edit this file directly. +// See scripts/generate-inspector/README.md for information on how to update the protocol definitions. +// Changes to the module itself should be added to the generator template (scripts/generate-inspector/inspector.d.ts.template). + +declare module "inspector" { + interface InspectorNotification { + method: string; + params: T; + } + + namespace Schema { + /** + * Description of the protocol domain. + */ + interface Domain { + /** + * Domain name. + */ + name: string; + /** + * Domain version. + */ + version: string; + } + interface GetDomainsReturnType { + /** + * List of supported domains. + */ + domains: Domain[]; + } + } + namespace Runtime { + /** + * Unique script identifier. + */ + type ScriptId = string; + /** + * Unique object identifier. + */ + type RemoteObjectId = string; + /** + * Primitive value which cannot be JSON-stringified. + */ + type UnserializableValue = string; + /** + * Mirror object referencing original JavaScript object. + */ + interface RemoteObject { + /** + * Object type. + */ + type: string; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + /** + * Object class (constructor) name. Specified for object type values only. + */ + className?: string | undefined; + /** + * Remote object value in case of primitive values or JSON values (if it was requested). + */ + value?: any; + /** + * Primitive value which can not be JSON-stringified does not have value, but gets this property. + */ + unserializableValue?: UnserializableValue | undefined; + /** + * String representation of the object. + */ + description?: string | undefined; + /** + * Unique object identifier (for non-primitive values). + */ + objectId?: RemoteObjectId | undefined; + /** + * Preview containing abbreviated property values. Specified for object type values only. + * @experimental + */ + preview?: ObjectPreview | undefined; + /** + * @experimental + */ + customPreview?: CustomPreview | undefined; + } + /** + * @experimental + */ + interface CustomPreview { + header: string; + hasBody: boolean; + formatterObjectId: RemoteObjectId; + bindRemoteObjectFunctionId: RemoteObjectId; + configObjectId?: RemoteObjectId | undefined; + } + /** + * Object containing abbreviated remote object value. + * @experimental + */ + interface ObjectPreview { + /** + * Object type. + */ + type: string; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + /** + * String representation of the object. + */ + description?: string | undefined; + /** + * True iff some of the properties or entries of the original object did not fit. + */ + overflow: boolean; + /** + * List of the properties. + */ + properties: PropertyPreview[]; + /** + * List of the entries. Specified for map and set subtype values only. + */ + entries?: EntryPreview[] | undefined; + } + /** + * @experimental + */ + interface PropertyPreview { + /** + * Property name. + */ + name: string; + /** + * Object type. Accessor means that the property itself is an accessor property. + */ + type: string; + /** + * User-friendly property value string. + */ + value?: string | undefined; + /** + * Nested value preview. + */ + valuePreview?: ObjectPreview | undefined; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + } + /** + * @experimental + */ + interface EntryPreview { + /** + * Preview of the key. Specified for map-like collection entries. + */ + key?: ObjectPreview | undefined; + /** + * Preview of the value. + */ + value: ObjectPreview; + } + /** + * Object property descriptor. + */ + interface PropertyDescriptor { + /** + * Property name or symbol description. + */ + name: string; + /** + * The value associated with the property. + */ + value?: RemoteObject | undefined; + /** + * True if the value associated with the property may be changed (data descriptors only). + */ + writable?: boolean | undefined; + /** + * A function which serves as a getter for the property, or undefined if there is no getter (accessor descriptors only). + */ + get?: RemoteObject | undefined; + /** + * A function which serves as a setter for the property, or undefined if there is no setter (accessor descriptors only). + */ + set?: RemoteObject | undefined; + /** + * True if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. + */ + configurable: boolean; + /** + * True if this property shows up during enumeration of the properties on the corresponding object. + */ + enumerable: boolean; + /** + * True if the result was thrown during the evaluation. + */ + wasThrown?: boolean | undefined; + /** + * True if the property is owned for the object. + */ + isOwn?: boolean | undefined; + /** + * Property symbol object, if the property is of the symbol type. + */ + symbol?: RemoteObject | undefined; + } + /** + * Object internal property descriptor. This property isn't normally visible in JavaScript code. + */ + interface InternalPropertyDescriptor { + /** + * Conventional property name. + */ + name: string; + /** + * The value associated with the property. + */ + value?: RemoteObject | undefined; + } + /** + * Represents function call argument. Either remote object id objectId, primitive value, unserializable primitive value or neither of (for undefined) them should be specified. + */ + interface CallArgument { + /** + * Primitive value or serializable javascript object. + */ + value?: any; + /** + * Primitive value which can not be JSON-stringified. + */ + unserializableValue?: UnserializableValue | undefined; + /** + * Remote object handle. + */ + objectId?: RemoteObjectId | undefined; + } + /** + * Id of an execution context. + */ + type ExecutionContextId = number; + /** + * Description of an isolated world. + */ + interface ExecutionContextDescription { + /** + * Unique id of the execution context. It can be used to specify in which execution context script evaluation should be performed. + */ + id: ExecutionContextId; + /** + * Execution context origin. + */ + origin: string; + /** + * Human readable name describing given context. + */ + name: string; + /** + * Embedder-specific auxiliary data. + */ + auxData?: object | undefined; + } + /** + * Detailed information about exception (or error) that was thrown during script compilation or execution. + */ + interface ExceptionDetails { + /** + * Exception id. + */ + exceptionId: number; + /** + * Exception text, which should be used together with exception object when available. + */ + text: string; + /** + * Line number of the exception location (0-based). + */ + lineNumber: number; + /** + * Column number of the exception location (0-based). + */ + columnNumber: number; + /** + * Script ID of the exception location. + */ + scriptId?: ScriptId | undefined; + /** + * URL of the exception location, to be used when the script was not reported. + */ + url?: string | undefined; + /** + * JavaScript stack trace if available. + */ + stackTrace?: StackTrace | undefined; + /** + * Exception object if available. + */ + exception?: RemoteObject | undefined; + /** + * Identifier of the context where exception happened. + */ + executionContextId?: ExecutionContextId | undefined; + } + /** + * Number of milliseconds since epoch. + */ + type Timestamp = number; + /** + * Stack entry for runtime errors and assertions. + */ + interface CallFrame { + /** + * JavaScript function name. + */ + functionName: string; + /** + * JavaScript script id. + */ + scriptId: ScriptId; + /** + * JavaScript script name or url. + */ + url: string; + /** + * JavaScript script line number (0-based). + */ + lineNumber: number; + /** + * JavaScript script column number (0-based). + */ + columnNumber: number; + } + /** + * Call frames for assertions or error messages. + */ + interface StackTrace { + /** + * String label of this stack trace. For async traces this may be a name of the function that initiated the async call. + */ + description?: string | undefined; + /** + * JavaScript function name. + */ + callFrames: CallFrame[]; + /** + * Asynchronous JavaScript stack trace that preceded this stack, if available. + */ + parent?: StackTrace | undefined; + /** + * Asynchronous JavaScript stack trace that preceded this stack, if available. + * @experimental + */ + parentId?: StackTraceId | undefined; + } + /** + * Unique identifier of current debugger. + * @experimental + */ + type UniqueDebuggerId = string; + /** + * If debuggerId is set stack trace comes from another debugger and can be resolved there. This allows to track cross-debugger calls. See Runtime.StackTrace and Debugger.paused for usages. + * @experimental + */ + interface StackTraceId { + id: string; + debuggerId?: UniqueDebuggerId | undefined; + } + interface EvaluateParameterType { + /** + * Expression to evaluate. + */ + expression: string; + /** + * Symbolic group name that can be used to release multiple objects. + */ + objectGroup?: string | undefined; + /** + * Determines whether Command Line API should be available during the evaluation. + */ + includeCommandLineAPI?: boolean | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Specifies in which execution context to perform evaluation. If the parameter is omitted the evaluation will be performed in the context of the inspected page. + */ + contextId?: ExecutionContextId | undefined; + /** + * Whether the result is expected to be a JSON object that should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + * @experimental + */ + generatePreview?: boolean | undefined; + /** + * Whether execution should be treated as initiated by user in the UI. + */ + userGesture?: boolean | undefined; + /** + * Whether execution should await for resulting value and return once awaited promise is resolved. + */ + awaitPromise?: boolean | undefined; + } + interface AwaitPromiseParameterType { + /** + * Identifier of the promise. + */ + promiseObjectId: RemoteObjectId; + /** + * Whether the result is expected to be a JSON object that should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + */ + generatePreview?: boolean | undefined; + } + interface CallFunctionOnParameterType { + /** + * Declaration of the function to call. + */ + functionDeclaration: string; + /** + * Identifier of the object to call function on. Either objectId or executionContextId should be specified. + */ + objectId?: RemoteObjectId | undefined; + /** + * Call arguments. All call arguments must belong to the same JavaScript world as the target object. + */ + arguments?: CallArgument[] | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Whether the result is expected to be a JSON object which should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + * @experimental + */ + generatePreview?: boolean | undefined; + /** + * Whether execution should be treated as initiated by user in the UI. + */ + userGesture?: boolean | undefined; + /** + * Whether execution should await for resulting value and return once awaited promise is resolved. + */ + awaitPromise?: boolean | undefined; + /** + * Specifies execution context which global object will be used to call function on. Either executionContextId or objectId should be specified. + */ + executionContextId?: ExecutionContextId | undefined; + /** + * Symbolic group name that can be used to release multiple objects. If objectGroup is not specified and objectId is, objectGroup will be inherited from object. + */ + objectGroup?: string | undefined; + } + interface GetPropertiesParameterType { + /** + * Identifier of the object to return properties for. + */ + objectId: RemoteObjectId; + /** + * If true, returns properties belonging only to the element itself, not to its prototype chain. + */ + ownProperties?: boolean | undefined; + /** + * If true, returns accessor properties (with getter/setter) only; internal properties are not returned either. + * @experimental + */ + accessorPropertiesOnly?: boolean | undefined; + /** + * Whether preview should be generated for the results. + * @experimental + */ + generatePreview?: boolean | undefined; + } + interface ReleaseObjectParameterType { + /** + * Identifier of the object to release. + */ + objectId: RemoteObjectId; + } + interface ReleaseObjectGroupParameterType { + /** + * Symbolic object group name. + */ + objectGroup: string; + } + interface SetCustomObjectFormatterEnabledParameterType { + enabled: boolean; + } + interface CompileScriptParameterType { + /** + * Expression to compile. + */ + expression: string; + /** + * Source url to be set for the script. + */ + sourceURL: string; + /** + * Specifies whether the compiled script should be persisted. + */ + persistScript: boolean; + /** + * Specifies in which execution context to perform script run. If the parameter is omitted the evaluation will be performed in the context of the inspected page. + */ + executionContextId?: ExecutionContextId | undefined; + } + interface RunScriptParameterType { + /** + * Id of the script to run. + */ + scriptId: ScriptId; + /** + * Specifies in which execution context to perform script run. If the parameter is omitted the evaluation will be performed in the context of the inspected page. + */ + executionContextId?: ExecutionContextId | undefined; + /** + * Symbolic group name that can be used to release multiple objects. + */ + objectGroup?: string | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Determines whether Command Line API should be available during the evaluation. + */ + includeCommandLineAPI?: boolean | undefined; + /** + * Whether the result is expected to be a JSON object which should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + */ + generatePreview?: boolean | undefined; + /** + * Whether execution should await for resulting value and return once awaited promise is resolved. + */ + awaitPromise?: boolean | undefined; + } + interface QueryObjectsParameterType { + /** + * Identifier of the prototype to return objects for. + */ + prototypeObjectId: RemoteObjectId; + } + interface GlobalLexicalScopeNamesParameterType { + /** + * Specifies in which execution context to lookup global scope variables. + */ + executionContextId?: ExecutionContextId | undefined; + } + interface EvaluateReturnType { + /** + * Evaluation result. + */ + result: RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface AwaitPromiseReturnType { + /** + * Promise result. Will contain rejected value if promise was rejected. + */ + result: RemoteObject; + /** + * Exception details if stack strace is available. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface CallFunctionOnReturnType { + /** + * Call result. + */ + result: RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface GetPropertiesReturnType { + /** + * Object properties. + */ + result: PropertyDescriptor[]; + /** + * Internal object properties (only of the element itself). + */ + internalProperties?: InternalPropertyDescriptor[] | undefined; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface CompileScriptReturnType { + /** + * Id of the script. + */ + scriptId?: ScriptId | undefined; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface RunScriptReturnType { + /** + * Run result. + */ + result: RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface QueryObjectsReturnType { + /** + * Array with objects. + */ + objects: RemoteObject; + } + interface GlobalLexicalScopeNamesReturnType { + names: string[]; + } + interface ExecutionContextCreatedEventDataType { + /** + * A newly created execution context. + */ + context: ExecutionContextDescription; + } + interface ExecutionContextDestroyedEventDataType { + /** + * Id of the destroyed context + */ + executionContextId: ExecutionContextId; + } + interface ExceptionThrownEventDataType { + /** + * Timestamp of the exception. + */ + timestamp: Timestamp; + exceptionDetails: ExceptionDetails; + } + interface ExceptionRevokedEventDataType { + /** + * Reason describing why exception was revoked. + */ + reason: string; + /** + * The id of revoked exception, as reported in exceptionThrown. + */ + exceptionId: number; + } + interface ConsoleAPICalledEventDataType { + /** + * Type of the call. + */ + type: string; + /** + * Call arguments. + */ + args: RemoteObject[]; + /** + * Identifier of the context where the call was made. + */ + executionContextId: ExecutionContextId; + /** + * Call timestamp. + */ + timestamp: Timestamp; + /** + * Stack trace captured when the call was made. + */ + stackTrace?: StackTrace | undefined; + /** + * Console context descriptor for calls on non-default console context (not console.*): 'anonymous#unique-logger-id' for call on unnamed context, 'name#unique-logger-id' for call on named context. + * @experimental + */ + context?: string | undefined; + } + interface InspectRequestedEventDataType { + object: RemoteObject; + hints: object; + } + } + namespace Debugger { + /** + * Breakpoint identifier. + */ + type BreakpointId = string; + /** + * Call frame identifier. + */ + type CallFrameId = string; + /** + * Location in the source code. + */ + interface Location { + /** + * Script identifier as reported in the Debugger.scriptParsed. + */ + scriptId: Runtime.ScriptId; + /** + * Line number in the script (0-based). + */ + lineNumber: number; + /** + * Column number in the script (0-based). + */ + columnNumber?: number | undefined; + } + /** + * Location in the source code. + * @experimental + */ + interface ScriptPosition { + lineNumber: number; + columnNumber: number; + } + /** + * JavaScript call frame. Array of call frames form the call stack. + */ + interface CallFrame { + /** + * Call frame identifier. This identifier is only valid while the virtual machine is paused. + */ + callFrameId: CallFrameId; + /** + * Name of the JavaScript function called on this call frame. + */ + functionName: string; + /** + * Location in the source code. + */ + functionLocation?: Location | undefined; + /** + * Location in the source code. + */ + location: Location; + /** + * JavaScript script name or url. + */ + url: string; + /** + * Scope chain for this call frame. + */ + scopeChain: Scope[]; + /** + * this object for this call frame. + */ + this: Runtime.RemoteObject; + /** + * The value being returned, if the function is at return point. + */ + returnValue?: Runtime.RemoteObject | undefined; + } + /** + * Scope description. + */ + interface Scope { + /** + * Scope type. + */ + type: string; + /** + * Object representing the scope. For global and with scopes it represents the actual object; for the rest of the scopes, it is artificial transient object enumerating scope variables as its properties. + */ + object: Runtime.RemoteObject; + name?: string | undefined; + /** + * Location in the source code where scope starts + */ + startLocation?: Location | undefined; + /** + * Location in the source code where scope ends + */ + endLocation?: Location | undefined; + } + /** + * Search match for resource. + */ + interface SearchMatch { + /** + * Line number in resource content. + */ + lineNumber: number; + /** + * Line with match content. + */ + lineContent: string; + } + interface BreakLocation { + /** + * Script identifier as reported in the Debugger.scriptParsed. + */ + scriptId: Runtime.ScriptId; + /** + * Line number in the script (0-based). + */ + lineNumber: number; + /** + * Column number in the script (0-based). + */ + columnNumber?: number | undefined; + type?: string | undefined; + } + interface SetBreakpointsActiveParameterType { + /** + * New value for breakpoints active state. + */ + active: boolean; + } + interface SetSkipAllPausesParameterType { + /** + * New value for skip pauses state. + */ + skip: boolean; + } + interface SetBreakpointByUrlParameterType { + /** + * Line number to set breakpoint at. + */ + lineNumber: number; + /** + * URL of the resources to set breakpoint on. + */ + url?: string | undefined; + /** + * Regex pattern for the URLs of the resources to set breakpoints on. Either url or urlRegex must be specified. + */ + urlRegex?: string | undefined; + /** + * Script hash of the resources to set breakpoint on. + */ + scriptHash?: string | undefined; + /** + * Offset in the line to set breakpoint at. + */ + columnNumber?: number | undefined; + /** + * Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true. + */ + condition?: string | undefined; + } + interface SetBreakpointParameterType { + /** + * Location to set breakpoint in. + */ + location: Location; + /** + * Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true. + */ + condition?: string | undefined; + } + interface RemoveBreakpointParameterType { + breakpointId: BreakpointId; + } + interface GetPossibleBreakpointsParameterType { + /** + * Start of range to search possible breakpoint locations in. + */ + start: Location; + /** + * End of range to search possible breakpoint locations in (excluding). When not specified, end of scripts is used as end of range. + */ + end?: Location | undefined; + /** + * Only consider locations which are in the same (non-nested) function as start. + */ + restrictToFunction?: boolean | undefined; + } + interface ContinueToLocationParameterType { + /** + * Location to continue to. + */ + location: Location; + targetCallFrames?: string | undefined; + } + interface PauseOnAsyncCallParameterType { + /** + * Debugger will pause when async call with given stack trace is started. + */ + parentStackTraceId: Runtime.StackTraceId; + } + interface StepIntoParameterType { + /** + * Debugger will issue additional Debugger.paused notification if any async task is scheduled before next pause. + * @experimental + */ + breakOnAsyncCall?: boolean | undefined; + } + interface GetStackTraceParameterType { + stackTraceId: Runtime.StackTraceId; + } + interface SearchInContentParameterType { + /** + * Id of the script to search in. + */ + scriptId: Runtime.ScriptId; + /** + * String to search for. + */ + query: string; + /** + * If true, search is case sensitive. + */ + caseSensitive?: boolean | undefined; + /** + * If true, treats string parameter as regex. + */ + isRegex?: boolean | undefined; + } + interface SetScriptSourceParameterType { + /** + * Id of the script to edit. + */ + scriptId: Runtime.ScriptId; + /** + * New content of the script. + */ + scriptSource: string; + /** + * If true the change will not actually be applied. Dry run may be used to get result description without actually modifying the code. + */ + dryRun?: boolean | undefined; + } + interface RestartFrameParameterType { + /** + * Call frame identifier to evaluate on. + */ + callFrameId: CallFrameId; + } + interface GetScriptSourceParameterType { + /** + * Id of the script to get source for. + */ + scriptId: Runtime.ScriptId; + } + interface SetPauseOnExceptionsParameterType { + /** + * Pause on exceptions mode. + */ + state: string; + } + interface EvaluateOnCallFrameParameterType { + /** + * Call frame identifier to evaluate on. + */ + callFrameId: CallFrameId; + /** + * Expression to evaluate. + */ + expression: string; + /** + * String object group name to put result into (allows rapid releasing resulting object handles using releaseObjectGroup). + */ + objectGroup?: string | undefined; + /** + * Specifies whether command line API should be available to the evaluated expression, defaults to false. + */ + includeCommandLineAPI?: boolean | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Whether the result is expected to be a JSON object that should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + * @experimental + */ + generatePreview?: boolean | undefined; + /** + * Whether to throw an exception if side effect cannot be ruled out during evaluation. + */ + throwOnSideEffect?: boolean | undefined; + } + interface SetVariableValueParameterType { + /** + * 0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch' scope types are allowed. Other scopes could be manipulated manually. + */ + scopeNumber: number; + /** + * Variable name. + */ + variableName: string; + /** + * New variable value. + */ + newValue: Runtime.CallArgument; + /** + * Id of callframe that holds variable. + */ + callFrameId: CallFrameId; + } + interface SetReturnValueParameterType { + /** + * New return value. + */ + newValue: Runtime.CallArgument; + } + interface SetAsyncCallStackDepthParameterType { + /** + * Maximum depth of async call stacks. Setting to 0 will effectively disable collecting async call stacks (default). + */ + maxDepth: number; + } + interface SetBlackboxPatternsParameterType { + /** + * Array of regexps that will be used to check script url for blackbox state. + */ + patterns: string[]; + } + interface SetBlackboxedRangesParameterType { + /** + * Id of the script. + */ + scriptId: Runtime.ScriptId; + positions: ScriptPosition[]; + } + interface EnableReturnType { + /** + * Unique identifier of the debugger. + * @experimental + */ + debuggerId: Runtime.UniqueDebuggerId; + } + interface SetBreakpointByUrlReturnType { + /** + * Id of the created breakpoint for further reference. + */ + breakpointId: BreakpointId; + /** + * List of the locations this breakpoint resolved into upon addition. + */ + locations: Location[]; + } + interface SetBreakpointReturnType { + /** + * Id of the created breakpoint for further reference. + */ + breakpointId: BreakpointId; + /** + * Location this breakpoint resolved into. + */ + actualLocation: Location; + } + interface GetPossibleBreakpointsReturnType { + /** + * List of the possible breakpoint locations. + */ + locations: BreakLocation[]; + } + interface GetStackTraceReturnType { + stackTrace: Runtime.StackTrace; + } + interface SearchInContentReturnType { + /** + * List of search matches. + */ + result: SearchMatch[]; + } + interface SetScriptSourceReturnType { + /** + * New stack trace in case editing has happened while VM was stopped. + */ + callFrames?: CallFrame[] | undefined; + /** + * Whether current call stack was modified after applying the changes. + */ + stackChanged?: boolean | undefined; + /** + * Async stack trace, if any. + */ + asyncStackTrace?: Runtime.StackTrace | undefined; + /** + * Async stack trace, if any. + * @experimental + */ + asyncStackTraceId?: Runtime.StackTraceId | undefined; + /** + * Exception details if any. + */ + exceptionDetails?: Runtime.ExceptionDetails | undefined; + } + interface RestartFrameReturnType { + /** + * New stack trace. + */ + callFrames: CallFrame[]; + /** + * Async stack trace, if any. + */ + asyncStackTrace?: Runtime.StackTrace | undefined; + /** + * Async stack trace, if any. + * @experimental + */ + asyncStackTraceId?: Runtime.StackTraceId | undefined; + } + interface GetScriptSourceReturnType { + /** + * Script source. + */ + scriptSource: string; + } + interface EvaluateOnCallFrameReturnType { + /** + * Object wrapper for the evaluation result. + */ + result: Runtime.RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: Runtime.ExceptionDetails | undefined; + } + interface ScriptParsedEventDataType { + /** + * Identifier of the script parsed. + */ + scriptId: Runtime.ScriptId; + /** + * URL or name of the script parsed (if any). + */ + url: string; + /** + * Line offset of the script within the resource with given URL (for script tags). + */ + startLine: number; + /** + * Column offset of the script within the resource with given URL. + */ + startColumn: number; + /** + * Last line of the script. + */ + endLine: number; + /** + * Length of the last line of the script. + */ + endColumn: number; + /** + * Specifies script creation context. + */ + executionContextId: Runtime.ExecutionContextId; + /** + * Content hash of the script. + */ + hash: string; + /** + * Embedder-specific auxiliary data. + */ + executionContextAuxData?: object | undefined; + /** + * True, if this script is generated as a result of the live edit operation. + * @experimental + */ + isLiveEdit?: boolean | undefined; + /** + * URL of source map associated with script (if any). + */ + sourceMapURL?: string | undefined; + /** + * True, if this script has sourceURL. + */ + hasSourceURL?: boolean | undefined; + /** + * True, if this script is ES6 module. + */ + isModule?: boolean | undefined; + /** + * This script length. + */ + length?: number | undefined; + /** + * JavaScript top stack frame of where the script parsed event was triggered if available. + * @experimental + */ + stackTrace?: Runtime.StackTrace | undefined; + } + interface ScriptFailedToParseEventDataType { + /** + * Identifier of the script parsed. + */ + scriptId: Runtime.ScriptId; + /** + * URL or name of the script parsed (if any). + */ + url: string; + /** + * Line offset of the script within the resource with given URL (for script tags). + */ + startLine: number; + /** + * Column offset of the script within the resource with given URL. + */ + startColumn: number; + /** + * Last line of the script. + */ + endLine: number; + /** + * Length of the last line of the script. + */ + endColumn: number; + /** + * Specifies script creation context. + */ + executionContextId: Runtime.ExecutionContextId; + /** + * Content hash of the script. + */ + hash: string; + /** + * Embedder-specific auxiliary data. + */ + executionContextAuxData?: object | undefined; + /** + * URL of source map associated with script (if any). + */ + sourceMapURL?: string | undefined; + /** + * True, if this script has sourceURL. + */ + hasSourceURL?: boolean | undefined; + /** + * True, if this script is ES6 module. + */ + isModule?: boolean | undefined; + /** + * This script length. + */ + length?: number | undefined; + /** + * JavaScript top stack frame of where the script parsed event was triggered if available. + * @experimental + */ + stackTrace?: Runtime.StackTrace | undefined; + } + interface BreakpointResolvedEventDataType { + /** + * Breakpoint unique identifier. + */ + breakpointId: BreakpointId; + /** + * Actual breakpoint location. + */ + location: Location; + } + interface PausedEventDataType { + /** + * Call stack the virtual machine stopped on. + */ + callFrames: CallFrame[]; + /** + * Pause reason. + */ + reason: string; + /** + * Object containing break-specific auxiliary properties. + */ + data?: object | undefined; + /** + * Hit breakpoints IDs + */ + hitBreakpoints?: string[] | undefined; + /** + * Async stack trace, if any. + */ + asyncStackTrace?: Runtime.StackTrace | undefined; + /** + * Async stack trace, if any. + * @experimental + */ + asyncStackTraceId?: Runtime.StackTraceId | undefined; + /** + * Just scheduled async call will have this stack trace as parent stack during async execution. This field is available only after Debugger.stepInto call with breakOnAsynCall flag. + * @experimental + */ + asyncCallStackTraceId?: Runtime.StackTraceId | undefined; + } + } + namespace Console { + /** + * Console message. + */ + interface ConsoleMessage { + /** + * Message source. + */ + source: string; + /** + * Message severity. + */ + level: string; + /** + * Message text. + */ + text: string; + /** + * URL of the message origin. + */ + url?: string | undefined; + /** + * Line number in the resource that generated this message (1-based). + */ + line?: number | undefined; + /** + * Column number in the resource that generated this message (1-based). + */ + column?: number | undefined; + } + interface MessageAddedEventDataType { + /** + * Console message that has been added. + */ + message: ConsoleMessage; + } + } + namespace Profiler { + /** + * Profile node. Holds callsite information, execution statistics and child nodes. + */ + interface ProfileNode { + /** + * Unique id of the node. + */ + id: number; + /** + * Function location. + */ + callFrame: Runtime.CallFrame; + /** + * Number of samples where this node was on top of the call stack. + */ + hitCount?: number | undefined; + /** + * Child node ids. + */ + children?: number[] | undefined; + /** + * The reason of being not optimized. The function may be deoptimized or marked as don't optimize. + */ + deoptReason?: string | undefined; + /** + * An array of source position ticks. + */ + positionTicks?: PositionTickInfo[] | undefined; + } + /** + * Profile. + */ + interface Profile { + /** + * The list of profile nodes. First item is the root node. + */ + nodes: ProfileNode[]; + /** + * Profiling start timestamp in microseconds. + */ + startTime: number; + /** + * Profiling end timestamp in microseconds. + */ + endTime: number; + /** + * Ids of samples top nodes. + */ + samples?: number[] | undefined; + /** + * Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime. + */ + timeDeltas?: number[] | undefined; + } + /** + * Specifies a number of samples attributed to a certain source position. + */ + interface PositionTickInfo { + /** + * Source line number (1-based). + */ + line: number; + /** + * Number of samples attributed to the source line. + */ + ticks: number; + } + /** + * Coverage data for a source range. + */ + interface CoverageRange { + /** + * JavaScript script source offset for the range start. + */ + startOffset: number; + /** + * JavaScript script source offset for the range end. + */ + endOffset: number; + /** + * Collected execution count of the source range. + */ + count: number; + } + /** + * Coverage data for a JavaScript function. + */ + interface FunctionCoverage { + /** + * JavaScript function name. + */ + functionName: string; + /** + * Source ranges inside the function with coverage data. + */ + ranges: CoverageRange[]; + /** + * Whether coverage data for this function has block granularity. + */ + isBlockCoverage: boolean; + } + /** + * Coverage data for a JavaScript script. + */ + interface ScriptCoverage { + /** + * JavaScript script id. + */ + scriptId: Runtime.ScriptId; + /** + * JavaScript script name or url. + */ + url: string; + /** + * Functions contained in the script that has coverage data. + */ + functions: FunctionCoverage[]; + } + interface SetSamplingIntervalParameterType { + /** + * New sampling interval in microseconds. + */ + interval: number; + } + interface StartPreciseCoverageParameterType { + /** + * Collect accurate call counts beyond simple 'covered' or 'not covered'. + */ + callCount?: boolean | undefined; + /** + * Collect block-based coverage. + */ + detailed?: boolean | undefined; + } + interface StopReturnType { + /** + * Recorded profile. + */ + profile: Profile; + } + interface TakePreciseCoverageReturnType { + /** + * Coverage data for the current isolate. + */ + result: ScriptCoverage[]; + } + interface GetBestEffortCoverageReturnType { + /** + * Coverage data for the current isolate. + */ + result: ScriptCoverage[]; + } + interface ConsoleProfileStartedEventDataType { + id: string; + /** + * Location of console.profile(). + */ + location: Debugger.Location; + /** + * Profile title passed as an argument to console.profile(). + */ + title?: string | undefined; + } + interface ConsoleProfileFinishedEventDataType { + id: string; + /** + * Location of console.profileEnd(). + */ + location: Debugger.Location; + profile: Profile; + /** + * Profile title passed as an argument to console.profile(). + */ + title?: string | undefined; + } + } + namespace HeapProfiler { + /** + * Heap snapshot object id. + */ + type HeapSnapshotObjectId = string; + /** + * Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes. + */ + interface SamplingHeapProfileNode { + /** + * Function location. + */ + callFrame: Runtime.CallFrame; + /** + * Allocations size in bytes for the node excluding children. + */ + selfSize: number; + /** + * Child nodes. + */ + children: SamplingHeapProfileNode[]; + } + /** + * Profile. + */ + interface SamplingHeapProfile { + head: SamplingHeapProfileNode; + } + interface StartTrackingHeapObjectsParameterType { + trackAllocations?: boolean | undefined; + } + interface StopTrackingHeapObjectsParameterType { + /** + * If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken when the tracking is stopped. + */ + reportProgress?: boolean | undefined; + } + interface TakeHeapSnapshotParameterType { + /** + * If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken. + */ + reportProgress?: boolean | undefined; + } + interface GetObjectByHeapObjectIdParameterType { + objectId: HeapSnapshotObjectId; + /** + * Symbolic group name that can be used to release multiple objects. + */ + objectGroup?: string | undefined; + } + interface AddInspectedHeapObjectParameterType { + /** + * Heap snapshot object id to be accessible by means of $x command line API. + */ + heapObjectId: HeapSnapshotObjectId; + } + interface GetHeapObjectIdParameterType { + /** + * Identifier of the object to get heap object id for. + */ + objectId: Runtime.RemoteObjectId; + } + interface StartSamplingParameterType { + /** + * Average sample interval in bytes. Poisson distribution is used for the intervals. The default value is 32768 bytes. + */ + samplingInterval?: number | undefined; + } + interface GetObjectByHeapObjectIdReturnType { + /** + * Evaluation result. + */ + result: Runtime.RemoteObject; + } + interface GetHeapObjectIdReturnType { + /** + * Id of the heap snapshot object corresponding to the passed remote object id. + */ + heapSnapshotObjectId: HeapSnapshotObjectId; + } + interface StopSamplingReturnType { + /** + * Recorded sampling heap profile. + */ + profile: SamplingHeapProfile; + } + interface GetSamplingProfileReturnType { + /** + * Return the sampling profile being collected. + */ + profile: SamplingHeapProfile; + } + interface AddHeapSnapshotChunkEventDataType { + chunk: string; + } + interface ReportHeapSnapshotProgressEventDataType { + done: number; + total: number; + finished?: boolean | undefined; + } + interface LastSeenObjectIdEventDataType { + lastSeenObjectId: number; + timestamp: number; + } + interface HeapStatsUpdateEventDataType { + /** + * An array of triplets. Each triplet describes a fragment. The first integer is the fragment index, the second integer is a total count of objects for the fragment, the third integer is a total size of the objects for the fragment. + */ + statsUpdate: number[]; + } + } + namespace NodeTracing { + interface TraceConfig { + /** + * Controls how the trace buffer stores data. + */ + recordMode?: string | undefined; + /** + * Included category filters. + */ + includedCategories: string[]; + } + interface StartParameterType { + traceConfig: TraceConfig; + } + interface GetCategoriesReturnType { + /** + * A list of supported tracing categories. + */ + categories: string[]; + } + interface DataCollectedEventDataType { + value: object[]; + } + } + namespace NodeWorker { + type WorkerID = string; + /** + * Unique identifier of attached debugging session. + */ + type SessionID = string; + interface WorkerInfo { + workerId: WorkerID; + type: string; + title: string; + url: string; + } + interface SendMessageToWorkerParameterType { + message: string; + /** + * Identifier of the session. + */ + sessionId: SessionID; + } + interface EnableParameterType { + /** + * Whether to new workers should be paused until the frontend sends `Runtime.runIfWaitingForDebugger` + * message to run them. + */ + waitForDebuggerOnStart: boolean; + } + interface DetachParameterType { + sessionId: SessionID; + } + interface AttachedToWorkerEventDataType { + /** + * Identifier assigned to the session used to send/receive messages. + */ + sessionId: SessionID; + workerInfo: WorkerInfo; + waitingForDebugger: boolean; + } + interface DetachedFromWorkerEventDataType { + /** + * Detached session identifier. + */ + sessionId: SessionID; + } + interface ReceivedMessageFromWorkerEventDataType { + /** + * Identifier of a session which sends a message. + */ + sessionId: SessionID; + message: string; + } + } + namespace Network { + /** + * Resource type as it was perceived by the rendering engine. + */ + type ResourceType = string; + /** + * Unique request identifier. + */ + type RequestId = string; + /** + * UTC time in seconds, counted from January 1, 1970. + */ + type TimeSinceEpoch = number; + /** + * Monotonically increasing time in seconds since an arbitrary point in the past. + */ + type MonotonicTime = number; + /** + * Information about the request initiator. + */ + interface Initiator { + /** + * Type of this initiator. + */ + type: string; + /** + * Initiator JavaScript stack trace, set for Script only. + * Requires the Debugger domain to be enabled. + */ + stack?: Runtime.StackTrace | undefined; + /** + * Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type. + */ + url?: string | undefined; + /** + * Initiator line number, set for Parser type or for Script type (when script is importing + * module) (0-based). + */ + lineNumber?: number | undefined; + /** + * Initiator column number, set for Parser type or for Script type (when script is importing + * module) (0-based). + */ + columnNumber?: number | undefined; + /** + * Set if another request triggered this request (e.g. preflight). + */ + requestId?: RequestId | undefined; + } + /** + * HTTP request data. + */ + interface Request { + url: string; + method: string; + headers: Headers; + hasPostData: boolean; + } + /** + * HTTP response data. + */ + interface Response { + url: string; + status: number; + statusText: string; + headers: Headers; + mimeType: string; + charset: string; + } + /** + * Request / response headers as keys / values of JSON object. + */ + interface Headers { + } + interface LoadNetworkResourcePageResult { + success: boolean; + stream?: IO.StreamHandle | undefined; + } + interface GetRequestPostDataParameterType { + /** + * Identifier of the network request to get content for. + */ + requestId: RequestId; + } + interface GetResponseBodyParameterType { + /** + * Identifier of the network request to get content for. + */ + requestId: RequestId; + } + interface StreamResourceContentParameterType { + /** + * Identifier of the request to stream. + */ + requestId: RequestId; + } + interface LoadNetworkResourceParameterType { + /** + * URL of the resource to get content for. + */ + url: string; + } + interface GetRequestPostDataReturnType { + /** + * Request body string, omitting files from multipart requests + */ + postData: string; + } + interface GetResponseBodyReturnType { + /** + * Response body. + */ + body: string; + /** + * True, if content was sent as base64. + */ + base64Encoded: boolean; + } + interface StreamResourceContentReturnType { + /** + * Data that has been buffered until streaming is enabled. + */ + bufferedData: string; + } + interface LoadNetworkResourceReturnType { + resource: LoadNetworkResourcePageResult; + } + interface RequestWillBeSentEventDataType { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Request data. + */ + request: Request; + /** + * Request initiator. + */ + initiator: Initiator; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * Timestamp. + */ + wallTime: TimeSinceEpoch; + } + interface ResponseReceivedEventDataType { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * Resource type. + */ + type: ResourceType; + /** + * Response data. + */ + response: Response; + } + interface LoadingFailedEventDataType { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * Resource type. + */ + type: ResourceType; + /** + * Error message. + */ + errorText: string; + } + interface LoadingFinishedEventDataType { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + } + interface DataReceivedEventDataType { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * Data chunk length. + */ + dataLength: number; + /** + * Actual bytes received (might be less than dataLength for compressed encodings). + */ + encodedDataLength: number; + /** + * Data that was received. + * @experimental + */ + data?: string | undefined; + } + } + namespace NodeRuntime { + interface NotifyWhenWaitingForDisconnectParameterType { + enabled: boolean; + } + } + namespace Target { + type SessionID = string; + type TargetID = string; + interface TargetInfo { + targetId: TargetID; + type: string; + title: string; + url: string; + attached: boolean; + canAccessOpener: boolean; + } + interface SetAutoAttachParameterType { + autoAttach: boolean; + waitForDebuggerOnStart: boolean; + } + interface TargetCreatedEventDataType { + targetInfo: TargetInfo; + } + interface AttachedToTargetEventDataType { + sessionId: SessionID; + targetInfo: TargetInfo; + waitingForDebugger: boolean; + } + } + namespace IO { + type StreamHandle = string; + interface ReadParameterType { + /** + * Handle of the stream to read. + */ + handle: StreamHandle; + /** + * Seek to the specified offset before reading (if not specified, proceed with offset + * following the last read). Some types of streams may only support sequential reads. + */ + offset?: number | undefined; + /** + * Maximum number of bytes to read (left upon the agent discretion if not specified). + */ + size?: number | undefined; + } + interface CloseParameterType { + /** + * Handle of the stream to close. + */ + handle: StreamHandle; + } + interface ReadReturnType { + /** + * Data that were read. + */ + data: string; + /** + * Set if the end-of-file condition occurred while reading. + */ + eof: boolean; + } + } + + interface Session { + /** + * Posts a message to the inspector back-end. `callback` will be notified when + * a response is received. `callback` is a function that accepts two optional + * arguments: error and message-specific result. + * + * ```js + * session.post('Runtime.evaluate', { expression: '2 + 2' }, + * (error, { result }) => console.log(result)); + * // Output: { type: 'number', value: 4, description: '4' } + * ``` + * + * The latest version of the V8 inspector protocol is published on the + * [Chrome DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/v8/). + * + * Node.js inspector supports all the Chrome DevTools Protocol domains declared + * by V8. Chrome DevTools Protocol domain provides an interface for interacting + * with one of the runtime agents used to inspect the application state and listen + * to the run-time events. + */ + post(method: string, callback?: (err: Error | null, params?: object) => void): void; + post(method: string, params?: object, callback?: (err: Error | null, params?: object) => void): void; + /** + * Returns supported domains. + */ + post(method: "Schema.getDomains", callback?: (err: Error | null, params: Schema.GetDomainsReturnType) => void): void; + /** + * Evaluates expression on global object. + */ + post(method: "Runtime.evaluate", params?: Runtime.EvaluateParameterType, callback?: (err: Error | null, params: Runtime.EvaluateReturnType) => void): void; + post(method: "Runtime.evaluate", callback?: (err: Error | null, params: Runtime.EvaluateReturnType) => void): void; + /** + * Add handler to promise with given promise object id. + */ + post(method: "Runtime.awaitPromise", params?: Runtime.AwaitPromiseParameterType, callback?: (err: Error | null, params: Runtime.AwaitPromiseReturnType) => void): void; + post(method: "Runtime.awaitPromise", callback?: (err: Error | null, params: Runtime.AwaitPromiseReturnType) => void): void; + /** + * Calls function with given declaration on the given object. Object group of the result is inherited from the target object. + */ + post(method: "Runtime.callFunctionOn", params?: Runtime.CallFunctionOnParameterType, callback?: (err: Error | null, params: Runtime.CallFunctionOnReturnType) => void): void; + post(method: "Runtime.callFunctionOn", callback?: (err: Error | null, params: Runtime.CallFunctionOnReturnType) => void): void; + /** + * Returns properties of a given object. Object group of the result is inherited from the target object. + */ + post(method: "Runtime.getProperties", params?: Runtime.GetPropertiesParameterType, callback?: (err: Error | null, params: Runtime.GetPropertiesReturnType) => void): void; + post(method: "Runtime.getProperties", callback?: (err: Error | null, params: Runtime.GetPropertiesReturnType) => void): void; + /** + * Releases remote object with given id. + */ + post(method: "Runtime.releaseObject", params?: Runtime.ReleaseObjectParameterType, callback?: (err: Error | null) => void): void; + post(method: "Runtime.releaseObject", callback?: (err: Error | null) => void): void; + /** + * Releases all remote objects that belong to a given group. + */ + post(method: "Runtime.releaseObjectGroup", params?: Runtime.ReleaseObjectGroupParameterType, callback?: (err: Error | null) => void): void; + post(method: "Runtime.releaseObjectGroup", callback?: (err: Error | null) => void): void; + /** + * Tells inspected instance to run if it was waiting for debugger to attach. + */ + post(method: "Runtime.runIfWaitingForDebugger", callback?: (err: Error | null) => void): void; + /** + * Enables reporting of execution contexts creation by means of executionContextCreated event. When the reporting gets enabled the event will be sent immediately for each existing execution context. + */ + post(method: "Runtime.enable", callback?: (err: Error | null) => void): void; + /** + * Disables reporting of execution contexts creation. + */ + post(method: "Runtime.disable", callback?: (err: Error | null) => void): void; + /** + * Discards collected exceptions and console API calls. + */ + post(method: "Runtime.discardConsoleEntries", callback?: (err: Error | null) => void): void; + /** + * @experimental + */ + post(method: "Runtime.setCustomObjectFormatterEnabled", params?: Runtime.SetCustomObjectFormatterEnabledParameterType, callback?: (err: Error | null) => void): void; + post(method: "Runtime.setCustomObjectFormatterEnabled", callback?: (err: Error | null) => void): void; + /** + * Compiles expression. + */ + post(method: "Runtime.compileScript", params?: Runtime.CompileScriptParameterType, callback?: (err: Error | null, params: Runtime.CompileScriptReturnType) => void): void; + post(method: "Runtime.compileScript", callback?: (err: Error | null, params: Runtime.CompileScriptReturnType) => void): void; + /** + * Runs script with given id in a given context. + */ + post(method: "Runtime.runScript", params?: Runtime.RunScriptParameterType, callback?: (err: Error | null, params: Runtime.RunScriptReturnType) => void): void; + post(method: "Runtime.runScript", callback?: (err: Error | null, params: Runtime.RunScriptReturnType) => void): void; + post(method: "Runtime.queryObjects", params?: Runtime.QueryObjectsParameterType, callback?: (err: Error | null, params: Runtime.QueryObjectsReturnType) => void): void; + post(method: "Runtime.queryObjects", callback?: (err: Error | null, params: Runtime.QueryObjectsReturnType) => void): void; + /** + * Returns all let, const and class variables from global scope. + */ + post( + method: "Runtime.globalLexicalScopeNames", + params?: Runtime.GlobalLexicalScopeNamesParameterType, + callback?: (err: Error | null, params: Runtime.GlobalLexicalScopeNamesReturnType) => void + ): void; + post(method: "Runtime.globalLexicalScopeNames", callback?: (err: Error | null, params: Runtime.GlobalLexicalScopeNamesReturnType) => void): void; + /** + * Enables debugger for the given page. Clients should not assume that the debugging has been enabled until the result for this command is received. + */ + post(method: "Debugger.enable", callback?: (err: Error | null, params: Debugger.EnableReturnType) => void): void; + /** + * Disables debugger for given page. + */ + post(method: "Debugger.disable", callback?: (err: Error | null) => void): void; + /** + * Activates / deactivates all breakpoints on the page. + */ + post(method: "Debugger.setBreakpointsActive", params?: Debugger.SetBreakpointsActiveParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setBreakpointsActive", callback?: (err: Error | null) => void): void; + /** + * Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc). + */ + post(method: "Debugger.setSkipAllPauses", params?: Debugger.SetSkipAllPausesParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setSkipAllPauses", callback?: (err: Error | null) => void): void; + /** + * Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this command is issued, all existing parsed scripts will have breakpoints resolved and returned in locations property. Further matching script parsing will result in subsequent breakpointResolved events issued. This logical breakpoint will survive page reloads. + */ + post(method: "Debugger.setBreakpointByUrl", params?: Debugger.SetBreakpointByUrlParameterType, callback?: (err: Error | null, params: Debugger.SetBreakpointByUrlReturnType) => void): void; + post(method: "Debugger.setBreakpointByUrl", callback?: (err: Error | null, params: Debugger.SetBreakpointByUrlReturnType) => void): void; + /** + * Sets JavaScript breakpoint at a given location. + */ + post(method: "Debugger.setBreakpoint", params?: Debugger.SetBreakpointParameterType, callback?: (err: Error | null, params: Debugger.SetBreakpointReturnType) => void): void; + post(method: "Debugger.setBreakpoint", callback?: (err: Error | null, params: Debugger.SetBreakpointReturnType) => void): void; + /** + * Removes JavaScript breakpoint. + */ + post(method: "Debugger.removeBreakpoint", params?: Debugger.RemoveBreakpointParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.removeBreakpoint", callback?: (err: Error | null) => void): void; + /** + * Returns possible locations for breakpoint. scriptId in start and end range locations should be the same. + */ + post( + method: "Debugger.getPossibleBreakpoints", + params?: Debugger.GetPossibleBreakpointsParameterType, + callback?: (err: Error | null, params: Debugger.GetPossibleBreakpointsReturnType) => void + ): void; + post(method: "Debugger.getPossibleBreakpoints", callback?: (err: Error | null, params: Debugger.GetPossibleBreakpointsReturnType) => void): void; + /** + * Continues execution until specific location is reached. + */ + post(method: "Debugger.continueToLocation", params?: Debugger.ContinueToLocationParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.continueToLocation", callback?: (err: Error | null) => void): void; + /** + * @experimental + */ + post(method: "Debugger.pauseOnAsyncCall", params?: Debugger.PauseOnAsyncCallParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.pauseOnAsyncCall", callback?: (err: Error | null) => void): void; + /** + * Steps over the statement. + */ + post(method: "Debugger.stepOver", callback?: (err: Error | null) => void): void; + /** + * Steps into the function call. + */ + post(method: "Debugger.stepInto", params?: Debugger.StepIntoParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.stepInto", callback?: (err: Error | null) => void): void; + /** + * Steps out of the function call. + */ + post(method: "Debugger.stepOut", callback?: (err: Error | null) => void): void; + /** + * Stops on the next JavaScript statement. + */ + post(method: "Debugger.pause", callback?: (err: Error | null) => void): void; + /** + * This method is deprecated - use Debugger.stepInto with breakOnAsyncCall and Debugger.pauseOnAsyncTask instead. Steps into next scheduled async task if any is scheduled before next pause. Returns success when async task is actually scheduled, returns error if no task were scheduled or another scheduleStepIntoAsync was called. + * @experimental + */ + post(method: "Debugger.scheduleStepIntoAsync", callback?: (err: Error | null) => void): void; + /** + * Resumes JavaScript execution. + */ + post(method: "Debugger.resume", callback?: (err: Error | null) => void): void; + /** + * Returns stack trace with given stackTraceId. + * @experimental + */ + post(method: "Debugger.getStackTrace", params?: Debugger.GetStackTraceParameterType, callback?: (err: Error | null, params: Debugger.GetStackTraceReturnType) => void): void; + post(method: "Debugger.getStackTrace", callback?: (err: Error | null, params: Debugger.GetStackTraceReturnType) => void): void; + /** + * Searches for given string in script content. + */ + post(method: "Debugger.searchInContent", params?: Debugger.SearchInContentParameterType, callback?: (err: Error | null, params: Debugger.SearchInContentReturnType) => void): void; + post(method: "Debugger.searchInContent", callback?: (err: Error | null, params: Debugger.SearchInContentReturnType) => void): void; + /** + * Edits JavaScript source live. + */ + post(method: "Debugger.setScriptSource", params?: Debugger.SetScriptSourceParameterType, callback?: (err: Error | null, params: Debugger.SetScriptSourceReturnType) => void): void; + post(method: "Debugger.setScriptSource", callback?: (err: Error | null, params: Debugger.SetScriptSourceReturnType) => void): void; + /** + * Restarts particular call frame from the beginning. + */ + post(method: "Debugger.restartFrame", params?: Debugger.RestartFrameParameterType, callback?: (err: Error | null, params: Debugger.RestartFrameReturnType) => void): void; + post(method: "Debugger.restartFrame", callback?: (err: Error | null, params: Debugger.RestartFrameReturnType) => void): void; + /** + * Returns source for the script with given id. + */ + post(method: "Debugger.getScriptSource", params?: Debugger.GetScriptSourceParameterType, callback?: (err: Error | null, params: Debugger.GetScriptSourceReturnType) => void): void; + post(method: "Debugger.getScriptSource", callback?: (err: Error | null, params: Debugger.GetScriptSourceReturnType) => void): void; + /** + * Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or no exceptions. Initial pause on exceptions state is none. + */ + post(method: "Debugger.setPauseOnExceptions", params?: Debugger.SetPauseOnExceptionsParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setPauseOnExceptions", callback?: (err: Error | null) => void): void; + /** + * Evaluates expression on a given call frame. + */ + post(method: "Debugger.evaluateOnCallFrame", params?: Debugger.EvaluateOnCallFrameParameterType, callback?: (err: Error | null, params: Debugger.EvaluateOnCallFrameReturnType) => void): void; + post(method: "Debugger.evaluateOnCallFrame", callback?: (err: Error | null, params: Debugger.EvaluateOnCallFrameReturnType) => void): void; + /** + * Changes value of variable in a callframe. Object-based scopes are not supported and must be mutated manually. + */ + post(method: "Debugger.setVariableValue", params?: Debugger.SetVariableValueParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setVariableValue", callback?: (err: Error | null) => void): void; + /** + * Changes return value in top frame. Available only at return break position. + * @experimental + */ + post(method: "Debugger.setReturnValue", params?: Debugger.SetReturnValueParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setReturnValue", callback?: (err: Error | null) => void): void; + /** + * Enables or disables async call stacks tracking. + */ + post(method: "Debugger.setAsyncCallStackDepth", params?: Debugger.SetAsyncCallStackDepthParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setAsyncCallStackDepth", callback?: (err: Error | null) => void): void; + /** + * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in scripts with url matching one of the patterns. VM will try to leave blackboxed script by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. + * @experimental + */ + post(method: "Debugger.setBlackboxPatterns", params?: Debugger.SetBlackboxPatternsParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setBlackboxPatterns", callback?: (err: Error | null) => void): void; + /** + * Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted scripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. Positions array contains positions where blackbox state is changed. First interval isn't blackboxed. Array should be sorted. + * @experimental + */ + post(method: "Debugger.setBlackboxedRanges", params?: Debugger.SetBlackboxedRangesParameterType, callback?: (err: Error | null) => void): void; + post(method: "Debugger.setBlackboxedRanges", callback?: (err: Error | null) => void): void; + /** + * Enables console domain, sends the messages collected so far to the client by means of the messageAdded notification. + */ + post(method: "Console.enable", callback?: (err: Error | null) => void): void; + /** + * Disables console domain, prevents further console messages from being reported to the client. + */ + post(method: "Console.disable", callback?: (err: Error | null) => void): void; + /** + * Does nothing. + */ + post(method: "Console.clearMessages", callback?: (err: Error | null) => void): void; + post(method: "Profiler.enable", callback?: (err: Error | null) => void): void; + post(method: "Profiler.disable", callback?: (err: Error | null) => void): void; + /** + * Changes CPU profiler sampling interval. Must be called before CPU profiles recording started. + */ + post(method: "Profiler.setSamplingInterval", params?: Profiler.SetSamplingIntervalParameterType, callback?: (err: Error | null) => void): void; + post(method: "Profiler.setSamplingInterval", callback?: (err: Error | null) => void): void; + post(method: "Profiler.start", callback?: (err: Error | null) => void): void; + post(method: "Profiler.stop", callback?: (err: Error | null, params: Profiler.StopReturnType) => void): void; + /** + * Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code coverage may be incomplete. Enabling prevents running optimized code and resets execution counters. + */ + post(method: "Profiler.startPreciseCoverage", params?: Profiler.StartPreciseCoverageParameterType, callback?: (err: Error | null) => void): void; + post(method: "Profiler.startPreciseCoverage", callback?: (err: Error | null) => void): void; + /** + * Disable precise code coverage. Disabling releases unnecessary execution count records and allows executing optimized code. + */ + post(method: "Profiler.stopPreciseCoverage", callback?: (err: Error | null) => void): void; + /** + * Collect coverage data for the current isolate, and resets execution counters. Precise code coverage needs to have started. + */ + post(method: "Profiler.takePreciseCoverage", callback?: (err: Error | null, params: Profiler.TakePreciseCoverageReturnType) => void): void; + /** + * Collect coverage data for the current isolate. The coverage data may be incomplete due to garbage collection. + */ + post(method: "Profiler.getBestEffortCoverage", callback?: (err: Error | null, params: Profiler.GetBestEffortCoverageReturnType) => void): void; + post(method: "HeapProfiler.enable", callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.disable", callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.startTrackingHeapObjects", params?: HeapProfiler.StartTrackingHeapObjectsParameterType, callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.startTrackingHeapObjects", callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.stopTrackingHeapObjects", params?: HeapProfiler.StopTrackingHeapObjectsParameterType, callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.stopTrackingHeapObjects", callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.takeHeapSnapshot", params?: HeapProfiler.TakeHeapSnapshotParameterType, callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.takeHeapSnapshot", callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.collectGarbage", callback?: (err: Error | null) => void): void; + post( + method: "HeapProfiler.getObjectByHeapObjectId", + params?: HeapProfiler.GetObjectByHeapObjectIdParameterType, + callback?: (err: Error | null, params: HeapProfiler.GetObjectByHeapObjectIdReturnType) => void + ): void; + post(method: "HeapProfiler.getObjectByHeapObjectId", callback?: (err: Error | null, params: HeapProfiler.GetObjectByHeapObjectIdReturnType) => void): void; + /** + * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). + */ + post(method: "HeapProfiler.addInspectedHeapObject", params?: HeapProfiler.AddInspectedHeapObjectParameterType, callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.addInspectedHeapObject", callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.getHeapObjectId", params?: HeapProfiler.GetHeapObjectIdParameterType, callback?: (err: Error | null, params: HeapProfiler.GetHeapObjectIdReturnType) => void): void; + post(method: "HeapProfiler.getHeapObjectId", callback?: (err: Error | null, params: HeapProfiler.GetHeapObjectIdReturnType) => void): void; + post(method: "HeapProfiler.startSampling", params?: HeapProfiler.StartSamplingParameterType, callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.startSampling", callback?: (err: Error | null) => void): void; + post(method: "HeapProfiler.stopSampling", callback?: (err: Error | null, params: HeapProfiler.StopSamplingReturnType) => void): void; + post(method: "HeapProfiler.getSamplingProfile", callback?: (err: Error | null, params: HeapProfiler.GetSamplingProfileReturnType) => void): void; + /** + * Gets supported tracing categories. + */ + post(method: "NodeTracing.getCategories", callback?: (err: Error | null, params: NodeTracing.GetCategoriesReturnType) => void): void; + /** + * Start trace events collection. + */ + post(method: "NodeTracing.start", params?: NodeTracing.StartParameterType, callback?: (err: Error | null) => void): void; + post(method: "NodeTracing.start", callback?: (err: Error | null) => void): void; + /** + * Stop trace events collection. Remaining collected events will be sent as a sequence of + * dataCollected events followed by tracingComplete event. + */ + post(method: "NodeTracing.stop", callback?: (err: Error | null) => void): void; + /** + * Sends protocol message over session with given id. + */ + post(method: "NodeWorker.sendMessageToWorker", params?: NodeWorker.SendMessageToWorkerParameterType, callback?: (err: Error | null) => void): void; + post(method: "NodeWorker.sendMessageToWorker", callback?: (err: Error | null) => void): void; + /** + * Instructs the inspector to attach to running workers. Will also attach to new workers + * as they start + */ + post(method: "NodeWorker.enable", params?: NodeWorker.EnableParameterType, callback?: (err: Error | null) => void): void; + post(method: "NodeWorker.enable", callback?: (err: Error | null) => void): void; + /** + * Detaches from all running workers and disables attaching to new workers as they are started. + */ + post(method: "NodeWorker.disable", callback?: (err: Error | null) => void): void; + /** + * Detached from the worker with given sessionId. + */ + post(method: "NodeWorker.detach", params?: NodeWorker.DetachParameterType, callback?: (err: Error | null) => void): void; + post(method: "NodeWorker.detach", callback?: (err: Error | null) => void): void; + /** + * Disables network tracking, prevents network events from being sent to the client. + */ + post(method: "Network.disable", callback?: (err: Error | null) => void): void; + /** + * Enables network tracking, network events will now be delivered to the client. + */ + post(method: "Network.enable", callback?: (err: Error | null) => void): void; + /** + * Returns post data sent with the request. Returns an error when no data was sent with the request. + */ + post(method: "Network.getRequestPostData", params?: Network.GetRequestPostDataParameterType, callback?: (err: Error | null, params: Network.GetRequestPostDataReturnType) => void): void; + post(method: "Network.getRequestPostData", callback?: (err: Error | null, params: Network.GetRequestPostDataReturnType) => void): void; + /** + * Returns content served for the given request. + */ + post(method: "Network.getResponseBody", params?: Network.GetResponseBodyParameterType, callback?: (err: Error | null, params: Network.GetResponseBodyReturnType) => void): void; + post(method: "Network.getResponseBody", callback?: (err: Error | null, params: Network.GetResponseBodyReturnType) => void): void; + /** + * Enables streaming of the response for the given requestId. + * If enabled, the dataReceived event contains the data that was received during streaming. + * @experimental + */ + post( + method: "Network.streamResourceContent", + params?: Network.StreamResourceContentParameterType, + callback?: (err: Error | null, params: Network.StreamResourceContentReturnType) => void + ): void; + post(method: "Network.streamResourceContent", callback?: (err: Error | null, params: Network.StreamResourceContentReturnType) => void): void; + /** + * Fetches the resource and returns the content. + */ + post(method: "Network.loadNetworkResource", params?: Network.LoadNetworkResourceParameterType, callback?: (err: Error | null, params: Network.LoadNetworkResourceReturnType) => void): void; + post(method: "Network.loadNetworkResource", callback?: (err: Error | null, params: Network.LoadNetworkResourceReturnType) => void): void; + /** + * Enable the NodeRuntime events except by `NodeRuntime.waitingForDisconnect`. + */ + post(method: "NodeRuntime.enable", callback?: (err: Error | null) => void): void; + /** + * Disable NodeRuntime events + */ + post(method: "NodeRuntime.disable", callback?: (err: Error | null) => void): void; + /** + * Enable the `NodeRuntime.waitingForDisconnect`. + */ + post(method: "NodeRuntime.notifyWhenWaitingForDisconnect", params?: NodeRuntime.NotifyWhenWaitingForDisconnectParameterType, callback?: (err: Error | null) => void): void; + post(method: "NodeRuntime.notifyWhenWaitingForDisconnect", callback?: (err: Error | null) => void): void; + post(method: "Target.setAutoAttach", params?: Target.SetAutoAttachParameterType, callback?: (err: Error | null) => void): void; + post(method: "Target.setAutoAttach", callback?: (err: Error | null) => void): void; + /** + * Read a chunk of the stream + */ + post(method: "IO.read", params?: IO.ReadParameterType, callback?: (err: Error | null, params: IO.ReadReturnType) => void): void; + post(method: "IO.read", callback?: (err: Error | null, params: IO.ReadReturnType) => void): void; + post(method: "IO.close", params?: IO.CloseParameterType, callback?: (err: Error | null) => void): void; + post(method: "IO.close", callback?: (err: Error | null) => void): void; + + addListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + addListener(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + addListener(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + addListener(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + addListener(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + addListener(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + addListener(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + addListener(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + addListener(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + addListener(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + addListener(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + addListener(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + addListener(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + addListener(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + addListener(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + addListener(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + addListener(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + addListener(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + addListener(event: "HeapProfiler.resetProfiles", listener: () => void): this; + addListener(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + addListener(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + addListener(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + addListener(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + addListener(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + addListener(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + addListener(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + addListener(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + addListener(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + addListener(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + addListener(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + addListener(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + addListener(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + addListener(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + addListener(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + addListener(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + addListener(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "inspectorNotification", message: InspectorNotification): boolean; + emit(event: "Runtime.executionContextCreated", message: InspectorNotification): boolean; + emit(event: "Runtime.executionContextDestroyed", message: InspectorNotification): boolean; + emit(event: "Runtime.executionContextsCleared"): boolean; + emit(event: "Runtime.exceptionThrown", message: InspectorNotification): boolean; + emit(event: "Runtime.exceptionRevoked", message: InspectorNotification): boolean; + emit(event: "Runtime.consoleAPICalled", message: InspectorNotification): boolean; + emit(event: "Runtime.inspectRequested", message: InspectorNotification): boolean; + emit(event: "Debugger.scriptParsed", message: InspectorNotification): boolean; + emit(event: "Debugger.scriptFailedToParse", message: InspectorNotification): boolean; + emit(event: "Debugger.breakpointResolved", message: InspectorNotification): boolean; + emit(event: "Debugger.paused", message: InspectorNotification): boolean; + emit(event: "Debugger.resumed"): boolean; + emit(event: "Console.messageAdded", message: InspectorNotification): boolean; + emit(event: "Profiler.consoleProfileStarted", message: InspectorNotification): boolean; + emit(event: "Profiler.consoleProfileFinished", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.addHeapSnapshotChunk", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.resetProfiles"): boolean; + emit(event: "HeapProfiler.reportHeapSnapshotProgress", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.lastSeenObjectId", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.heapStatsUpdate", message: InspectorNotification): boolean; + emit(event: "NodeTracing.dataCollected", message: InspectorNotification): boolean; + emit(event: "NodeTracing.tracingComplete"): boolean; + emit(event: "NodeWorker.attachedToWorker", message: InspectorNotification): boolean; + emit(event: "NodeWorker.detachedFromWorker", message: InspectorNotification): boolean; + emit(event: "NodeWorker.receivedMessageFromWorker", message: InspectorNotification): boolean; + emit(event: "Network.requestWillBeSent", message: InspectorNotification): boolean; + emit(event: "Network.responseReceived", message: InspectorNotification): boolean; + emit(event: "Network.loadingFailed", message: InspectorNotification): boolean; + emit(event: "Network.loadingFinished", message: InspectorNotification): boolean; + emit(event: "Network.dataReceived", message: InspectorNotification): boolean; + emit(event: "NodeRuntime.waitingForDisconnect"): boolean; + emit(event: "NodeRuntime.waitingForDebugger"): boolean; + emit(event: "Target.targetCreated", message: InspectorNotification): boolean; + emit(event: "Target.attachedToTarget", message: InspectorNotification): boolean; + on(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + on(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + on(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + on(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + on(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + on(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + on(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + on(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + on(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + on(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + on(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + on(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + on(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + on(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + on(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + on(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + on(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + on(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + on(event: "HeapProfiler.resetProfiles", listener: () => void): this; + on(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + on(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + on(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + on(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + on(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + on(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + on(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + on(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + on(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + on(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + on(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + on(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + on(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + on(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + on(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + on(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + on(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + once(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + once(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + once(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + once(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + once(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + once(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + once(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + once(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + once(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + once(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + once(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + once(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + once(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + once(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + once(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + once(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + once(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + once(event: "HeapProfiler.resetProfiles", listener: () => void): this; + once(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + once(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + once(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + once(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + once(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + once(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + once(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + once(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + once(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + once(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + once(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + once(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + once(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + once(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + once(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + once(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + once(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + prependListener(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + prependListener(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + prependListener(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + prependListener(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + prependListener(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + prependListener(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + prependListener(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + prependListener(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + prependListener(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + prependListener(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + prependListener(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + prependListener(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + prependListener(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + prependListener(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + prependListener(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + prependListener(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + prependListener(event: "HeapProfiler.resetProfiles", listener: () => void): this; + prependListener(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + prependListener(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + prependListener(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + prependListener(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + prependListener(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + prependListener(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + prependListener(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + prependListener(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + prependListener(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + prependListener(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + prependListener(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + prependListener(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + prependListener(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + prependListener(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + prependOnceListener(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + prependOnceListener(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + prependOnceListener(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + prependOnceListener(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + prependOnceListener(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + prependOnceListener(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + prependOnceListener(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + prependOnceListener(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + prependOnceListener(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + prependOnceListener(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + prependOnceListener(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + prependOnceListener(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + prependOnceListener(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + prependOnceListener(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + prependOnceListener(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "HeapProfiler.resetProfiles", listener: () => void): this; + prependOnceListener(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + prependOnceListener(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + prependOnceListener(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + prependOnceListener(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + prependOnceListener(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + prependOnceListener(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + prependOnceListener(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + prependOnceListener(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + prependOnceListener(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + prependOnceListener(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + prependOnceListener(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + prependOnceListener(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + prependOnceListener(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + prependOnceListener(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + } +} + +declare module "inspector/promises" { + export { + Schema, + Runtime, + Debugger, + Console, + Profiler, + HeapProfiler, + NodeTracing, + NodeWorker, + Network, + NodeRuntime, + Target, + IO, + } from 'inspector'; +} + +declare module "inspector/promises" { + import { + InspectorNotification, + Schema, + Runtime, + Debugger, + Console, + Profiler, + HeapProfiler, + NodeTracing, + NodeWorker, + Network, + NodeRuntime, + Target, + IO, + } from "inspector"; + + /** + * The `inspector.Session` is used for dispatching messages to the V8 inspector + * back-end and receiving message responses and notifications. + * @since v19.0.0 + */ + interface Session { + /** + * Posts a message to the inspector back-end. + * + * ```js + * import { Session } from 'node:inspector/promises'; + * try { + * const session = new Session(); + * session.connect(); + * const result = await session.post('Runtime.evaluate', { expression: '2 + 2' }); + * console.log(result); + * } catch (error) { + * console.error(error); + * } + * // Output: { result: { type: 'number', value: 4, description: '4' } } + * ``` + * + * The latest version of the V8 inspector protocol is published on the + * [Chrome DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/v8/). + * + * Node.js inspector supports all the Chrome DevTools Protocol domains declared + * by V8. Chrome DevTools Protocol domain provides an interface for interacting + * with one of the runtime agents used to inspect the application state and listen + * to the run-time events. + */ + post(method: string, params?: object): Promise; + /** + * Returns supported domains. + */ + post(method: "Schema.getDomains"): Promise; + /** + * Evaluates expression on global object. + */ + post(method: "Runtime.evaluate", params?: Runtime.EvaluateParameterType): Promise; + /** + * Add handler to promise with given promise object id. + */ + post(method: "Runtime.awaitPromise", params?: Runtime.AwaitPromiseParameterType): Promise; + /** + * Calls function with given declaration on the given object. Object group of the result is inherited from the target object. + */ + post(method: "Runtime.callFunctionOn", params?: Runtime.CallFunctionOnParameterType): Promise; + /** + * Returns properties of a given object. Object group of the result is inherited from the target object. + */ + post(method: "Runtime.getProperties", params?: Runtime.GetPropertiesParameterType): Promise; + /** + * Releases remote object with given id. + */ + post(method: "Runtime.releaseObject", params?: Runtime.ReleaseObjectParameterType): Promise; + /** + * Releases all remote objects that belong to a given group. + */ + post(method: "Runtime.releaseObjectGroup", params?: Runtime.ReleaseObjectGroupParameterType): Promise; + /** + * Tells inspected instance to run if it was waiting for debugger to attach. + */ + post(method: "Runtime.runIfWaitingForDebugger"): Promise; + /** + * Enables reporting of execution contexts creation by means of executionContextCreated event. When the reporting gets enabled the event will be sent immediately for each existing execution context. + */ + post(method: "Runtime.enable"): Promise; + /** + * Disables reporting of execution contexts creation. + */ + post(method: "Runtime.disable"): Promise; + /** + * Discards collected exceptions and console API calls. + */ + post(method: "Runtime.discardConsoleEntries"): Promise; + /** + * @experimental + */ + post(method: "Runtime.setCustomObjectFormatterEnabled", params?: Runtime.SetCustomObjectFormatterEnabledParameterType): Promise; + /** + * Compiles expression. + */ + post(method: "Runtime.compileScript", params?: Runtime.CompileScriptParameterType): Promise; + /** + * Runs script with given id in a given context. + */ + post(method: "Runtime.runScript", params?: Runtime.RunScriptParameterType): Promise; + post(method: "Runtime.queryObjects", params?: Runtime.QueryObjectsParameterType): Promise; + /** + * Returns all let, const and class variables from global scope. + */ + post(method: "Runtime.globalLexicalScopeNames", params?: Runtime.GlobalLexicalScopeNamesParameterType): Promise; + /** + * Enables debugger for the given page. Clients should not assume that the debugging has been enabled until the result for this command is received. + */ + post(method: "Debugger.enable"): Promise; + /** + * Disables debugger for given page. + */ + post(method: "Debugger.disable"): Promise; + /** + * Activates / deactivates all breakpoints on the page. + */ + post(method: "Debugger.setBreakpointsActive", params?: Debugger.SetBreakpointsActiveParameterType): Promise; + /** + * Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc). + */ + post(method: "Debugger.setSkipAllPauses", params?: Debugger.SetSkipAllPausesParameterType): Promise; + /** + * Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this command is issued, all existing parsed scripts will have breakpoints resolved and returned in locations property. Further matching script parsing will result in subsequent breakpointResolved events issued. This logical breakpoint will survive page reloads. + */ + post(method: "Debugger.setBreakpointByUrl", params?: Debugger.SetBreakpointByUrlParameterType): Promise; + /** + * Sets JavaScript breakpoint at a given location. + */ + post(method: "Debugger.setBreakpoint", params?: Debugger.SetBreakpointParameterType): Promise; + /** + * Removes JavaScript breakpoint. + */ + post(method: "Debugger.removeBreakpoint", params?: Debugger.RemoveBreakpointParameterType): Promise; + /** + * Returns possible locations for breakpoint. scriptId in start and end range locations should be the same. + */ + post(method: "Debugger.getPossibleBreakpoints", params?: Debugger.GetPossibleBreakpointsParameterType): Promise; + /** + * Continues execution until specific location is reached. + */ + post(method: "Debugger.continueToLocation", params?: Debugger.ContinueToLocationParameterType): Promise; + /** + * @experimental + */ + post(method: "Debugger.pauseOnAsyncCall", params?: Debugger.PauseOnAsyncCallParameterType): Promise; + /** + * Steps over the statement. + */ + post(method: "Debugger.stepOver"): Promise; + /** + * Steps into the function call. + */ + post(method: "Debugger.stepInto", params?: Debugger.StepIntoParameterType): Promise; + /** + * Steps out of the function call. + */ + post(method: "Debugger.stepOut"): Promise; + /** + * Stops on the next JavaScript statement. + */ + post(method: "Debugger.pause"): Promise; + /** + * This method is deprecated - use Debugger.stepInto with breakOnAsyncCall and Debugger.pauseOnAsyncTask instead. Steps into next scheduled async task if any is scheduled before next pause. Returns success when async task is actually scheduled, returns error if no task were scheduled or another scheduleStepIntoAsync was called. + * @experimental + */ + post(method: "Debugger.scheduleStepIntoAsync"): Promise; + /** + * Resumes JavaScript execution. + */ + post(method: "Debugger.resume"): Promise; + /** + * Returns stack trace with given stackTraceId. + * @experimental + */ + post(method: "Debugger.getStackTrace", params?: Debugger.GetStackTraceParameterType): Promise; + /** + * Searches for given string in script content. + */ + post(method: "Debugger.searchInContent", params?: Debugger.SearchInContentParameterType): Promise; + /** + * Edits JavaScript source live. + */ + post(method: "Debugger.setScriptSource", params?: Debugger.SetScriptSourceParameterType): Promise; + /** + * Restarts particular call frame from the beginning. + */ + post(method: "Debugger.restartFrame", params?: Debugger.RestartFrameParameterType): Promise; + /** + * Returns source for the script with given id. + */ + post(method: "Debugger.getScriptSource", params?: Debugger.GetScriptSourceParameterType): Promise; + /** + * Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or no exceptions. Initial pause on exceptions state is none. + */ + post(method: "Debugger.setPauseOnExceptions", params?: Debugger.SetPauseOnExceptionsParameterType): Promise; + /** + * Evaluates expression on a given call frame. + */ + post(method: "Debugger.evaluateOnCallFrame", params?: Debugger.EvaluateOnCallFrameParameterType): Promise; + /** + * Changes value of variable in a callframe. Object-based scopes are not supported and must be mutated manually. + */ + post(method: "Debugger.setVariableValue", params?: Debugger.SetVariableValueParameterType): Promise; + /** + * Changes return value in top frame. Available only at return break position. + * @experimental + */ + post(method: "Debugger.setReturnValue", params?: Debugger.SetReturnValueParameterType): Promise; + /** + * Enables or disables async call stacks tracking. + */ + post(method: "Debugger.setAsyncCallStackDepth", params?: Debugger.SetAsyncCallStackDepthParameterType): Promise; + /** + * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in scripts with url matching one of the patterns. VM will try to leave blackboxed script by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. + * @experimental + */ + post(method: "Debugger.setBlackboxPatterns", params?: Debugger.SetBlackboxPatternsParameterType): Promise; + /** + * Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted scripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. Positions array contains positions where blackbox state is changed. First interval isn't blackboxed. Array should be sorted. + * @experimental + */ + post(method: "Debugger.setBlackboxedRanges", params?: Debugger.SetBlackboxedRangesParameterType): Promise; + /** + * Enables console domain, sends the messages collected so far to the client by means of the messageAdded notification. + */ + post(method: "Console.enable"): Promise; + /** + * Disables console domain, prevents further console messages from being reported to the client. + */ + post(method: "Console.disable"): Promise; + /** + * Does nothing. + */ + post(method: "Console.clearMessages"): Promise; + post(method: "Profiler.enable"): Promise; + post(method: "Profiler.disable"): Promise; + /** + * Changes CPU profiler sampling interval. Must be called before CPU profiles recording started. + */ + post(method: "Profiler.setSamplingInterval", params?: Profiler.SetSamplingIntervalParameterType): Promise; + post(method: "Profiler.start"): Promise; + post(method: "Profiler.stop"): Promise; + /** + * Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code coverage may be incomplete. Enabling prevents running optimized code and resets execution counters. + */ + post(method: "Profiler.startPreciseCoverage", params?: Profiler.StartPreciseCoverageParameterType): Promise; + /** + * Disable precise code coverage. Disabling releases unnecessary execution count records and allows executing optimized code. + */ + post(method: "Profiler.stopPreciseCoverage"): Promise; + /** + * Collect coverage data for the current isolate, and resets execution counters. Precise code coverage needs to have started. + */ + post(method: "Profiler.takePreciseCoverage"): Promise; + /** + * Collect coverage data for the current isolate. The coverage data may be incomplete due to garbage collection. + */ + post(method: "Profiler.getBestEffortCoverage"): Promise; + post(method: "HeapProfiler.enable"): Promise; + post(method: "HeapProfiler.disable"): Promise; + post(method: "HeapProfiler.startTrackingHeapObjects", params?: HeapProfiler.StartTrackingHeapObjectsParameterType): Promise; + post(method: "HeapProfiler.stopTrackingHeapObjects", params?: HeapProfiler.StopTrackingHeapObjectsParameterType): Promise; + post(method: "HeapProfiler.takeHeapSnapshot", params?: HeapProfiler.TakeHeapSnapshotParameterType): Promise; + post(method: "HeapProfiler.collectGarbage"): Promise; + post(method: "HeapProfiler.getObjectByHeapObjectId", params?: HeapProfiler.GetObjectByHeapObjectIdParameterType): Promise; + /** + * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). + */ + post(method: "HeapProfiler.addInspectedHeapObject", params?: HeapProfiler.AddInspectedHeapObjectParameterType): Promise; + post(method: "HeapProfiler.getHeapObjectId", params?: HeapProfiler.GetHeapObjectIdParameterType): Promise; + post(method: "HeapProfiler.startSampling", params?: HeapProfiler.StartSamplingParameterType): Promise; + post(method: "HeapProfiler.stopSampling"): Promise; + post(method: "HeapProfiler.getSamplingProfile"): Promise; + /** + * Gets supported tracing categories. + */ + post(method: "NodeTracing.getCategories"): Promise; + /** + * Start trace events collection. + */ + post(method: "NodeTracing.start", params?: NodeTracing.StartParameterType): Promise; + /** + * Stop trace events collection. Remaining collected events will be sent as a sequence of + * dataCollected events followed by tracingComplete event. + */ + post(method: "NodeTracing.stop"): Promise; + /** + * Sends protocol message over session with given id. + */ + post(method: "NodeWorker.sendMessageToWorker", params?: NodeWorker.SendMessageToWorkerParameterType): Promise; + /** + * Instructs the inspector to attach to running workers. Will also attach to new workers + * as they start + */ + post(method: "NodeWorker.enable", params?: NodeWorker.EnableParameterType): Promise; + /** + * Detaches from all running workers and disables attaching to new workers as they are started. + */ + post(method: "NodeWorker.disable"): Promise; + /** + * Detached from the worker with given sessionId. + */ + post(method: "NodeWorker.detach", params?: NodeWorker.DetachParameterType): Promise; + /** + * Disables network tracking, prevents network events from being sent to the client. + */ + post(method: "Network.disable"): Promise; + /** + * Enables network tracking, network events will now be delivered to the client. + */ + post(method: "Network.enable"): Promise; + /** + * Returns post data sent with the request. Returns an error when no data was sent with the request. + */ + post(method: "Network.getRequestPostData", params?: Network.GetRequestPostDataParameterType): Promise; + /** + * Returns content served for the given request. + */ + post(method: "Network.getResponseBody", params?: Network.GetResponseBodyParameterType): Promise; + /** + * Enables streaming of the response for the given requestId. + * If enabled, the dataReceived event contains the data that was received during streaming. + * @experimental + */ + post(method: "Network.streamResourceContent", params?: Network.StreamResourceContentParameterType): Promise; + /** + * Fetches the resource and returns the content. + */ + post(method: "Network.loadNetworkResource", params?: Network.LoadNetworkResourceParameterType): Promise; + /** + * Enable the NodeRuntime events except by `NodeRuntime.waitingForDisconnect`. + */ + post(method: "NodeRuntime.enable"): Promise; + /** + * Disable NodeRuntime events + */ + post(method: "NodeRuntime.disable"): Promise; + /** + * Enable the `NodeRuntime.waitingForDisconnect`. + */ + post(method: "NodeRuntime.notifyWhenWaitingForDisconnect", params?: NodeRuntime.NotifyWhenWaitingForDisconnectParameterType): Promise; + post(method: "Target.setAutoAttach", params?: Target.SetAutoAttachParameterType): Promise; + /** + * Read a chunk of the stream + */ + post(method: "IO.read", params?: IO.ReadParameterType): Promise; + post(method: "IO.close", params?: IO.CloseParameterType): Promise; + + addListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + addListener(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + addListener(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + addListener(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + addListener(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + addListener(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + addListener(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + addListener(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + addListener(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + addListener(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + addListener(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + addListener(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + addListener(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + addListener(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + addListener(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + addListener(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + addListener(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + addListener(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + addListener(event: "HeapProfiler.resetProfiles", listener: () => void): this; + addListener(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + addListener(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + addListener(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + addListener(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + addListener(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + addListener(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + addListener(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + addListener(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + addListener(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + addListener(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + addListener(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + addListener(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + addListener(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + addListener(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + addListener(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + addListener(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + addListener(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "inspectorNotification", message: InspectorNotification): boolean; + emit(event: "Runtime.executionContextCreated", message: InspectorNotification): boolean; + emit(event: "Runtime.executionContextDestroyed", message: InspectorNotification): boolean; + emit(event: "Runtime.executionContextsCleared"): boolean; + emit(event: "Runtime.exceptionThrown", message: InspectorNotification): boolean; + emit(event: "Runtime.exceptionRevoked", message: InspectorNotification): boolean; + emit(event: "Runtime.consoleAPICalled", message: InspectorNotification): boolean; + emit(event: "Runtime.inspectRequested", message: InspectorNotification): boolean; + emit(event: "Debugger.scriptParsed", message: InspectorNotification): boolean; + emit(event: "Debugger.scriptFailedToParse", message: InspectorNotification): boolean; + emit(event: "Debugger.breakpointResolved", message: InspectorNotification): boolean; + emit(event: "Debugger.paused", message: InspectorNotification): boolean; + emit(event: "Debugger.resumed"): boolean; + emit(event: "Console.messageAdded", message: InspectorNotification): boolean; + emit(event: "Profiler.consoleProfileStarted", message: InspectorNotification): boolean; + emit(event: "Profiler.consoleProfileFinished", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.addHeapSnapshotChunk", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.resetProfiles"): boolean; + emit(event: "HeapProfiler.reportHeapSnapshotProgress", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.lastSeenObjectId", message: InspectorNotification): boolean; + emit(event: "HeapProfiler.heapStatsUpdate", message: InspectorNotification): boolean; + emit(event: "NodeTracing.dataCollected", message: InspectorNotification): boolean; + emit(event: "NodeTracing.tracingComplete"): boolean; + emit(event: "NodeWorker.attachedToWorker", message: InspectorNotification): boolean; + emit(event: "NodeWorker.detachedFromWorker", message: InspectorNotification): boolean; + emit(event: "NodeWorker.receivedMessageFromWorker", message: InspectorNotification): boolean; + emit(event: "Network.requestWillBeSent", message: InspectorNotification): boolean; + emit(event: "Network.responseReceived", message: InspectorNotification): boolean; + emit(event: "Network.loadingFailed", message: InspectorNotification): boolean; + emit(event: "Network.loadingFinished", message: InspectorNotification): boolean; + emit(event: "Network.dataReceived", message: InspectorNotification): boolean; + emit(event: "NodeRuntime.waitingForDisconnect"): boolean; + emit(event: "NodeRuntime.waitingForDebugger"): boolean; + emit(event: "Target.targetCreated", message: InspectorNotification): boolean; + emit(event: "Target.attachedToTarget", message: InspectorNotification): boolean; + on(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + on(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + on(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + on(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + on(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + on(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + on(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + on(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + on(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + on(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + on(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + on(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + on(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + on(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + on(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + on(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + on(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + on(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + on(event: "HeapProfiler.resetProfiles", listener: () => void): this; + on(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + on(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + on(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + on(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + on(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + on(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + on(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + on(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + on(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + on(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + on(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + on(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + on(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + on(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + on(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + on(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + on(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + once(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + once(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + once(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + once(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + once(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + once(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + once(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + once(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + once(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + once(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + once(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + once(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + once(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + once(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + once(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + once(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + once(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + once(event: "HeapProfiler.resetProfiles", listener: () => void): this; + once(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + once(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + once(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + once(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + once(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + once(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + once(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + once(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + once(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + once(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + once(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + once(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + once(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + once(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + once(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + once(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + once(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + prependListener(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + prependListener(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + prependListener(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + prependListener(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + prependListener(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + prependListener(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + prependListener(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + prependListener(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + prependListener(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + prependListener(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + prependListener(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + prependListener(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + prependListener(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + prependListener(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + prependListener(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + prependListener(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + prependListener(event: "HeapProfiler.resetProfiles", listener: () => void): this; + prependListener(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + prependListener(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + prependListener(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + prependListener(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + prependListener(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + prependListener(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + prependListener(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + prependListener(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + prependListener(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + prependListener(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + prependListener(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + prependListener(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + prependListener(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + prependListener(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + prependListener(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + prependOnceListener(event: "inspectorNotification", listener: (message: InspectorNotification) => void): this; + /** + * Issued when new execution context is created. + */ + prependOnceListener(event: "Runtime.executionContextCreated", listener: (message: InspectorNotification) => void): this; + /** + * Issued when execution context is destroyed. + */ + prependOnceListener(event: "Runtime.executionContextDestroyed", listener: (message: InspectorNotification) => void): this; + /** + * Issued when all executionContexts were cleared in browser + */ + prependOnceListener(event: "Runtime.executionContextsCleared", listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + prependOnceListener(event: "Runtime.exceptionThrown", listener: (message: InspectorNotification) => void): this; + /** + * Issued when unhandled exception was revoked. + */ + prependOnceListener(event: "Runtime.exceptionRevoked", listener: (message: InspectorNotification) => void): this; + /** + * Issued when console API was called. + */ + prependOnceListener(event: "Runtime.consoleAPICalled", listener: (message: InspectorNotification) => void): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + prependOnceListener(event: "Runtime.inspectRequested", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + prependOnceListener(event: "Debugger.scriptParsed", listener: (message: InspectorNotification) => void): this; + /** + * Fired when virtual machine fails to parse the script. + */ + prependOnceListener(event: "Debugger.scriptFailedToParse", listener: (message: InspectorNotification) => void): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + prependOnceListener(event: "Debugger.breakpointResolved", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + prependOnceListener(event: "Debugger.paused", listener: (message: InspectorNotification) => void): this; + /** + * Fired when the virtual machine resumed execution. + */ + prependOnceListener(event: "Debugger.resumed", listener: () => void): this; + /** + * Issued when new console message is added. + */ + prependOnceListener(event: "Console.messageAdded", listener: (message: InspectorNotification) => void): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + prependOnceListener(event: "Profiler.consoleProfileStarted", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Profiler.consoleProfileFinished", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "HeapProfiler.addHeapSnapshotChunk", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "HeapProfiler.resetProfiles", listener: () => void): this; + prependOnceListener(event: "HeapProfiler.reportHeapSnapshotProgress", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + prependOnceListener(event: "HeapProfiler.lastSeenObjectId", listener: (message: InspectorNotification) => void): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + prependOnceListener(event: "HeapProfiler.heapStatsUpdate", listener: (message: InspectorNotification) => void): this; + /** + * Contains an bucket of collected trace events. + */ + prependOnceListener(event: "NodeTracing.dataCollected", listener: (message: InspectorNotification) => void): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + prependOnceListener(event: "NodeTracing.tracingComplete", listener: () => void): this; + /** + * Issued when attached to a worker. + */ + prependOnceListener(event: "NodeWorker.attachedToWorker", listener: (message: InspectorNotification) => void): this; + /** + * Issued when detached from the worker. + */ + prependOnceListener(event: "NodeWorker.detachedFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + prependOnceListener(event: "NodeWorker.receivedMessageFromWorker", listener: (message: InspectorNotification) => void): this; + /** + * Fired when page is about to send HTTP request. + */ + prependOnceListener(event: "Network.requestWillBeSent", listener: (message: InspectorNotification) => void): this; + /** + * Fired when HTTP response is available. + */ + prependOnceListener(event: "Network.responseReceived", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Network.loadingFailed", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Network.loadingFinished", listener: (message: InspectorNotification) => void): this; + /** + * Fired when data chunk was received over the network. + */ + prependOnceListener(event: "Network.dataReceived", listener: (message: InspectorNotification) => void): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + prependOnceListener(event: "NodeRuntime.waitingForDisconnect", listener: () => void): this; + /** + * This event is fired when the runtime is waiting for the debugger. For + * example, when inspector.waitingForDebugger is called + */ + prependOnceListener(event: "NodeRuntime.waitingForDebugger", listener: () => void): this; + prependOnceListener(event: "Target.targetCreated", listener: (message: InspectorNotification) => void): this; + prependOnceListener(event: "Target.attachedToTarget", listener: (message: InspectorNotification) => void): this; + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/module.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/module.d.ts new file mode 100644 index 00000000..b48948e8 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/module.d.ts @@ -0,0 +1,891 @@ +/** + * @since v0.3.7 + */ +declare module "module" { + import { URL } from "node:url"; + class Module { + constructor(id: string, parent?: Module); + } + interface Module extends NodeJS.Module {} + namespace Module { + export { Module }; + } + namespace Module { + /** + * A list of the names of all modules provided by Node.js. Can be used to verify + * if a module is maintained by a third party or not. + * + * Note: the list doesn't contain prefix-only modules like `node:test`. + * @since v9.3.0, v8.10.0, v6.13.0 + */ + const builtinModules: readonly string[]; + /** + * @since v12.2.0 + * @param path Filename to be used to construct the require + * function. Must be a file URL object, file URL string, or absolute path + * string. + */ + function createRequire(path: string | URL): NodeJS.Require; + namespace constants { + /** + * The following constants are returned as the `status` field in the object returned by + * {@link enableCompileCache} to indicate the result of the attempt to enable the + * [module compile cache](https://nodejs.org/docs/latest-v22.x/api/module.html#module-compile-cache). + * @since v22.8.0 + */ + namespace compileCacheStatus { + /** + * Node.js has enabled the compile cache successfully. The directory used to store the + * compile cache will be returned in the `directory` field in the + * returned object. + */ + const ENABLED: number; + /** + * The compile cache has already been enabled before, either by a previous call to + * {@link enableCompileCache}, or by the `NODE_COMPILE_CACHE=dir` + * environment variable. The directory used to store the + * compile cache will be returned in the `directory` field in the + * returned object. + */ + const ALREADY_ENABLED: number; + /** + * Node.js fails to enable the compile cache. This can be caused by the lack of + * permission to use the specified directory, or various kinds of file system errors. + * The detail of the failure will be returned in the `message` field in the + * returned object. + */ + const FAILED: number; + /** + * Node.js cannot enable the compile cache because the environment variable + * `NODE_DISABLE_COMPILE_CACHE=1` has been set. + */ + const DISABLED: number; + } + } + interface EnableCompileCacheResult { + /** + * One of the {@link constants.compileCacheStatus} + */ + status: number; + /** + * If Node.js cannot enable the compile cache, this contains + * the error message. Only set if `status` is `module.constants.compileCacheStatus.FAILED`. + */ + message?: string; + /** + * If the compile cache is enabled, this contains the directory + * where the compile cache is stored. Only set if `status` is + * `module.constants.compileCacheStatus.ENABLED` or + * `module.constants.compileCacheStatus.ALREADY_ENABLED`. + */ + directory?: string; + } + /** + * Enable [module compile cache](https://nodejs.org/docs/latest-v22.x/api/module.html#module-compile-cache) + * in the current Node.js instance. + * + * If `cacheDir` is not specified, Node.js will either use the directory specified by the + * `NODE_COMPILE_CACHE=dir` environment variable if it's set, or use + * `path.join(os.tmpdir(), 'node-compile-cache')` otherwise. For general use cases, it's + * recommended to call `module.enableCompileCache()` without specifying the `cacheDir`, + * so that the directory can be overridden by the `NODE_COMPILE_CACHE` environment + * variable when necessary. + * + * Since compile cache is supposed to be a quiet optimization that is not required for the + * application to be functional, this method is designed to not throw any exception when the + * compile cache cannot be enabled. Instead, it will return an object containing an error + * message in the `message` field to aid debugging. + * If compile cache is enabled successfully, the `directory` field in the returned object + * contains the path to the directory where the compile cache is stored. The `status` + * field in the returned object would be one of the `module.constants.compileCacheStatus` + * values to indicate the result of the attempt to enable the + * [module compile cache](https://nodejs.org/docs/latest-v22.x/api/module.html#module-compile-cache). + * + * This method only affects the current Node.js instance. To enable it in child worker threads, + * either call this method in child worker threads too, or set the + * `process.env.NODE_COMPILE_CACHE` value to compile cache directory so the behavior can + * be inherited into the child workers. The directory can be obtained either from the + * `directory` field returned by this method, or with {@link getCompileCacheDir}. + * @since v22.8.0 + * @param cacheDir Optional path to specify the directory where the compile cache + * will be stored/retrieved. + */ + function enableCompileCache(cacheDir?: string): EnableCompileCacheResult; + /** + * Flush the [module compile cache](https://nodejs.org/docs/latest-v22.x/api/module.html#module-compile-cache) + * accumulated from modules already loaded + * in the current Node.js instance to disk. This returns after all the flushing + * file system operations come to an end, no matter they succeed or not. If there + * are any errors, this will fail silently, since compile cache misses should not + * interfere with the actual operation of the application. + * @since v22.10.0 + */ + function flushCompileCache(): void; + /** + * @since v22.8.0 + * @return Path to the [module compile cache](https://nodejs.org/docs/latest-v22.x/api/module.html#module-compile-cache) + * directory if it is enabled, or `undefined` otherwise. + */ + function getCompileCacheDir(): string | undefined; + /** + * ```text + * /path/to/project + * ├ packages/ + * ├ bar/ + * ├ bar.js + * └ package.json // name = '@foo/bar' + * └ qux/ + * ├ node_modules/ + * └ some-package/ + * └ package.json // name = 'some-package' + * ├ qux.js + * └ package.json // name = '@foo/qux' + * ├ main.js + * └ package.json // name = '@foo' + * ``` + * ```js + * // /path/to/project/packages/bar/bar.js + * import { findPackageJSON } from 'node:module'; + * + * findPackageJSON('..', import.meta.url); + * // '/path/to/project/package.json' + * // Same result when passing an absolute specifier instead: + * findPackageJSON(new URL('../', import.meta.url)); + * findPackageJSON(import.meta.resolve('../')); + * + * findPackageJSON('some-package', import.meta.url); + * // '/path/to/project/packages/bar/node_modules/some-package/package.json' + * // When passing an absolute specifier, you might get a different result if the + * // resolved module is inside a subfolder that has nested `package.json`. + * findPackageJSON(import.meta.resolve('some-package')); + * // '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json' + * + * findPackageJSON('@foo/qux', import.meta.url); + * // '/path/to/project/packages/qux/package.json' + * ``` + * @since v22.14.0 + * @param specifier The specifier for the module whose `package.json` to + * retrieve. When passing a _bare specifier_, the `package.json` at the root of + * the package is returned. When passing a _relative specifier_ or an _absolute specifier_, + * the closest parent `package.json` is returned. + * @param base The absolute location (`file:` URL string or FS path) of the + * containing module. For CJS, use `__filename` (not `__dirname`!); for ESM, use + * `import.meta.url`. You do not need to pass it if `specifier` is an _absolute specifier_. + * @returns A path if the `package.json` is found. When `startLocation` + * is a package, the package's root `package.json`; when a relative or unresolved, the closest + * `package.json` to the `startLocation`. + */ + function findPackageJSON(specifier: string | URL, base?: string | URL): string | undefined; + /** + * @since v18.6.0, v16.17.0 + */ + function isBuiltin(moduleName: string): boolean; + interface RegisterOptions { + /** + * If you want to resolve `specifier` relative to a + * base URL, such as `import.meta.url`, you can pass that URL here. This + * property is ignored if the `parentURL` is supplied as the second argument. + * @default 'data:' + */ + parentURL?: string | URL | undefined; + /** + * Any arbitrary, cloneable JavaScript value to pass into the + * {@link initialize} hook. + */ + data?: Data | undefined; + /** + * [Transferable objects](https://nodejs.org/docs/latest-v22.x/api/worker_threads.html#portpostmessagevalue-transferlist) + * to be passed into the `initialize` hook. + */ + transferList?: any[] | undefined; + } + /* eslint-disable @definitelytyped/no-unnecessary-generics */ + /** + * Register a module that exports hooks that customize Node.js module + * resolution and loading behavior. See + * [Customization hooks](https://nodejs.org/docs/latest-v22.x/api/module.html#customization-hooks). + * + * This feature requires `--allow-worker` if used with the + * [Permission Model](https://nodejs.org/docs/latest-v22.x/api/permissions.html#permission-model). + * @since v20.6.0, v18.19.0 + * @param specifier Customization hooks to be registered; this should be + * the same string that would be passed to `import()`, except that if it is + * relative, it is resolved relative to `parentURL`. + * @param parentURL f you want to resolve `specifier` relative to a base + * URL, such as `import.meta.url`, you can pass that URL here. + */ + function register( + specifier: string | URL, + parentURL?: string | URL, + options?: RegisterOptions, + ): void; + function register(specifier: string | URL, options?: RegisterOptions): void; + interface RegisterHooksOptions { + /** + * See [load hook](https://nodejs.org/docs/latest-v22.x/api/module.html#loadurl-context-nextload). + * @default undefined + */ + load?: LoadHookSync | undefined; + /** + * See [resolve hook](https://nodejs.org/docs/latest-v22.x/api/module.html#resolvespecifier-context-nextresolve). + * @default undefined + */ + resolve?: ResolveHookSync | undefined; + } + interface ModuleHooks { + /** + * Deregister the hook instance. + */ + deregister(): void; + } + /** + * Register [hooks](https://nodejs.org/docs/latest-v22.x/api/module.html#customization-hooks) + * that customize Node.js module resolution and loading behavior. + * @since v22.15.0 + * @experimental + */ + function registerHooks(options: RegisterHooksOptions): ModuleHooks; + interface StripTypeScriptTypesOptions { + /** + * Possible values are: + * * `'strip'` Only strip type annotations without performing the transformation of TypeScript features. + * * `'transform'` Strip type annotations and transform TypeScript features to JavaScript. + * @default 'strip' + */ + mode?: "strip" | "transform" | undefined; + /** + * Only when `mode` is `'transform'`, if `true`, a source map + * will be generated for the transformed code. + * @default false + */ + sourceMap?: boolean | undefined; + /** + * Specifies the source url used in the source map. + */ + sourceUrl?: string | undefined; + } + /** + * `module.stripTypeScriptTypes()` removes type annotations from TypeScript code. It + * can be used to strip type annotations from TypeScript code before running it + * with `vm.runInContext()` or `vm.compileFunction()`. + * By default, it will throw an error if the code contains TypeScript features + * that require transformation such as `Enums`, + * see [type-stripping](https://nodejs.org/docs/latest-v22.x/api/typescript.md#type-stripping) for more information. + * When mode is `'transform'`, it also transforms TypeScript features to JavaScript, + * see [transform TypeScript features](https://nodejs.org/docs/latest-v22.x/api/typescript.md#typescript-features) for more information. + * When mode is `'strip'`, source maps are not generated, because locations are preserved. + * If `sourceMap` is provided, when mode is `'strip'`, an error will be thrown. + * + * _WARNING_: The output of this function should not be considered stable across Node.js versions, + * due to changes in the TypeScript parser. + * + * ```js + * import { stripTypeScriptTypes } from 'node:module'; + * const code = 'const a: number = 1;'; + * const strippedCode = stripTypeScriptTypes(code); + * console.log(strippedCode); + * // Prints: const a = 1; + * ``` + * + * If `sourceUrl` is provided, it will be used appended as a comment at the end of the output: + * + * ```js + * import { stripTypeScriptTypes } from 'node:module'; + * const code = 'const a: number = 1;'; + * const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' }); + * console.log(strippedCode); + * // Prints: const a = 1\n\n//# sourceURL=source.ts; + * ``` + * + * When `mode` is `'transform'`, the code is transformed to JavaScript: + * + * ```js + * import { stripTypeScriptTypes } from 'node:module'; + * const code = ` + * namespace MathUtil { + * export const add = (a: number, b: number) => a + b; + * }`; + * const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true }); + * console.log(strippedCode); + * // Prints: + * // var MathUtil; + * // (function(MathUtil) { + * // MathUtil.add = (a, b)=>a + b; + * // })(MathUtil || (MathUtil = {})); + * // # sourceMappingURL=data:application/json;base64, ... + * ``` + * @since v22.13.0 + * @param code The code to strip type annotations from. + * @returns The code with type annotations stripped. + */ + function stripTypeScriptTypes(code: string, options?: StripTypeScriptTypesOptions): string; + /* eslint-enable @definitelytyped/no-unnecessary-generics */ + /** + * The `module.syncBuiltinESMExports()` method updates all the live bindings for + * builtin `ES Modules` to match the properties of the `CommonJS` exports. It + * does not add or remove exported names from the `ES Modules`. + * + * ```js + * import fs from 'node:fs'; + * import assert from 'node:assert'; + * import { syncBuiltinESMExports } from 'node:module'; + * + * fs.readFile = newAPI; + * + * delete fs.readFileSync; + * + * function newAPI() { + * // ... + * } + * + * fs.newAPI = newAPI; + * + * syncBuiltinESMExports(); + * + * import('node:fs').then((esmFS) => { + * // It syncs the existing readFile property with the new value + * assert.strictEqual(esmFS.readFile, newAPI); + * // readFileSync has been deleted from the required fs + * assert.strictEqual('readFileSync' in fs, false); + * // syncBuiltinESMExports() does not remove readFileSync from esmFS + * assert.strictEqual('readFileSync' in esmFS, true); + * // syncBuiltinESMExports() does not add names + * assert.strictEqual(esmFS.newAPI, undefined); + * }); + * ``` + * @since v12.12.0 + */ + function syncBuiltinESMExports(): void; + interface ImportAttributes extends NodeJS.Dict { + type?: string | undefined; + } + type ModuleFormat = + | "builtin" + | "commonjs" + | "commonjs-typescript" + | "json" + | "module" + | "module-typescript" + | "wasm"; + type ModuleSource = string | ArrayBuffer | NodeJS.TypedArray; + /** + * The `initialize` hook provides a way to define a custom function that runs in + * the hooks thread when the hooks module is initialized. Initialization happens + * when the hooks module is registered via {@link register}. + * + * This hook can receive data from a {@link register} invocation, including + * ports and other transferable objects. The return value of `initialize` can be a + * `Promise`, in which case it will be awaited before the main application thread + * execution resumes. + */ + type InitializeHook = (data: Data) => void | Promise; + interface ResolveHookContext { + /** + * Export conditions of the relevant `package.json` + */ + conditions: string[]; + /** + * An object whose key-value pairs represent the assertions for the module to import + */ + importAttributes: ImportAttributes; + /** + * The module importing this one, or undefined if this is the Node.js entry point + */ + parentURL: string | undefined; + } + interface ResolveFnOutput { + /** + * A hint to the load hook (it might be ignored); can be an intermediary value. + */ + format?: string | null | undefined; + /** + * The import attributes to use when caching the module (optional; if excluded the input will be used) + */ + importAttributes?: ImportAttributes | undefined; + /** + * A signal that this hook intends to terminate the chain of `resolve` hooks. + * @default false + */ + shortCircuit?: boolean | undefined; + /** + * The absolute URL to which this input resolves + */ + url: string; + } + /** + * The `resolve` hook chain is responsible for telling Node.js where to find and + * how to cache a given `import` statement or expression, or `require` call. It can + * optionally return a format (such as `'module'`) as a hint to the `load` hook. If + * a format is specified, the `load` hook is ultimately responsible for providing + * the final `format` value (and it is free to ignore the hint provided by + * `resolve`); if `resolve` provides a `format`, a custom `load` hook is required + * even if only to pass the value to the Node.js default `load` hook. + */ + type ResolveHook = ( + specifier: string, + context: ResolveHookContext, + nextResolve: ( + specifier: string, + context?: Partial, + ) => ResolveFnOutput | Promise, + ) => ResolveFnOutput | Promise; + type ResolveHookSync = ( + specifier: string, + context: ResolveHookContext, + nextResolve: ( + specifier: string, + context?: Partial, + ) => ResolveFnOutput, + ) => ResolveFnOutput; + interface LoadHookContext { + /** + * Export conditions of the relevant `package.json` + */ + conditions: string[]; + /** + * The format optionally supplied by the `resolve` hook chain (can be an intermediary value). + */ + format: string | null | undefined; + /** + * An object whose key-value pairs represent the assertions for the module to import + */ + importAttributes: ImportAttributes; + } + interface LoadFnOutput { + format: string | null | undefined; + /** + * A signal that this hook intends to terminate the chain of `resolve` hooks. + * @default false + */ + shortCircuit?: boolean | undefined; + /** + * The source for Node.js to evaluate + */ + source?: ModuleSource | undefined; + } + /** + * The `load` hook provides a way to define a custom method of determining how a + * URL should be interpreted, retrieved, and parsed. It is also in charge of + * validating the import attributes. + */ + type LoadHook = ( + url: string, + context: LoadHookContext, + nextLoad: ( + url: string, + context?: Partial, + ) => LoadFnOutput | Promise, + ) => LoadFnOutput | Promise; + type LoadHookSync = ( + url: string, + context: LoadHookContext, + nextLoad: ( + url: string, + context?: Partial, + ) => LoadFnOutput, + ) => LoadFnOutput; + interface SourceMapsSupport { + /** + * If the source maps support is enabled + */ + enabled: boolean; + /** + * If the support is enabled for files in `node_modules`. + */ + nodeModules: boolean; + /** + * If the support is enabled for generated code from `eval` or `new Function`. + */ + generatedCode: boolean; + } + /** + * This method returns whether the [Source Map v3](https://tc39.es/ecma426/) support for stack + * traces is enabled. + * @since v22.14.0 + */ + function getSourceMapsSupport(): SourceMapsSupport; + /** + * `path` is the resolved path for the file for which a corresponding source map + * should be fetched. + * @since v13.7.0, v12.17.0 + * @return Returns `module.SourceMap` if a source map is found, `undefined` otherwise. + */ + function findSourceMap(path: string): SourceMap | undefined; + interface SetSourceMapsSupportOptions { + /** + * If enabling the support for files in `node_modules`. + * @default false + */ + nodeModules?: boolean | undefined; + /** + * If enabling the support for generated code from `eval` or `new Function`. + * @default false + */ + generatedCode?: boolean | undefined; + } + /** + * This function enables or disables the [Source Map v3](https://tc39.es/ecma426/) support for + * stack traces. + * + * It provides same features as launching Node.js process with commandline options + * `--enable-source-maps`, with additional options to alter the support for files + * in `node_modules` or generated codes. + * + * Only source maps in JavaScript files that are loaded after source maps has been + * enabled will be parsed and loaded. Preferably, use the commandline options + * `--enable-source-maps` to avoid losing track of source maps of modules loaded + * before this API call. + * @since v22.14.0 + */ + function setSourceMapsSupport(enabled: boolean, options?: SetSourceMapsSupportOptions): void; + interface SourceMapConstructorOptions { + /** + * @since v21.0.0, v20.5.0 + */ + lineLengths?: readonly number[] | undefined; + } + interface SourceMapPayload { + file: string; + version: number; + sources: string[]; + sourcesContent: string[]; + names: string[]; + mappings: string; + sourceRoot: string; + } + interface SourceMapping { + generatedLine: number; + generatedColumn: number; + originalSource: string; + originalLine: number; + originalColumn: number; + } + interface SourceOrigin { + /** + * The name of the range in the source map, if one was provided + */ + name: string | undefined; + /** + * The file name of the original source, as reported in the SourceMap + */ + fileName: string; + /** + * The 1-indexed lineNumber of the corresponding call site in the original source + */ + lineNumber: number; + /** + * The 1-indexed columnNumber of the corresponding call site in the original source + */ + columnNumber: number; + } + /** + * @since v13.7.0, v12.17.0 + */ + class SourceMap { + constructor(payload: SourceMapPayload, options?: SourceMapConstructorOptions); + /** + * Getter for the payload used to construct the `SourceMap` instance. + */ + readonly payload: SourceMapPayload; + /** + * Given a line offset and column offset in the generated source + * file, returns an object representing the SourceMap range in the + * original file if found, or an empty object if not. + * + * The object returned contains the following keys: + * + * The returned value represents the raw range as it appears in the + * SourceMap, based on zero-indexed offsets, _not_ 1-indexed line and + * column numbers as they appear in Error messages and CallSite + * objects. + * + * To get the corresponding 1-indexed line and column numbers from a + * lineNumber and columnNumber as they are reported by Error stacks + * and CallSite objects, use `sourceMap.findOrigin(lineNumber, columnNumber)` + * @param lineOffset The zero-indexed line number offset in the generated source + * @param columnOffset The zero-indexed column number offset in the generated source + */ + findEntry(lineOffset: number, columnOffset: number): SourceMapping | {}; + /** + * Given a 1-indexed `lineNumber` and `columnNumber` from a call site in the generated source, + * find the corresponding call site location in the original source. + * + * If the `lineNumber` and `columnNumber` provided are not found in any source map, + * then an empty object is returned. + * @param lineNumber The 1-indexed line number of the call site in the generated source + * @param columnNumber The 1-indexed column number of the call site in the generated source + */ + findOrigin(lineNumber: number, columnNumber: number): SourceOrigin | {}; + } + function runMain(main?: string): void; + function wrap(script: string): string; + } + global { + interface ImportMeta { + /** + * The directory name of the current module. + * + * This is the same as the `path.dirname()` of the `import.meta.filename`. + * + * > **Caveat**: only present on `file:` modules. + * @since v21.2.0, v20.11.0 + */ + dirname: string; + /** + * The full absolute path and filename of the current module, with + * symlinks resolved. + * + * This is the same as the `url.fileURLToPath()` of the `import.meta.url`. + * + * > **Caveat** only local modules support this property. Modules not using the + * > `file:` protocol will not provide it. + * @since v21.2.0, v20.11.0 + */ + filename: string; + /** + * The absolute `file:` URL of the module. + * + * This is defined exactly the same as it is in browsers providing the URL of the + * current module file. + * + * This enables useful patterns such as relative file loading: + * + * ```js + * import { readFileSync } from 'node:fs'; + * const buffer = readFileSync(new URL('./data.proto', import.meta.url)); + * ``` + */ + url: string; + /** + * `import.meta.resolve` is a module-relative resolution function scoped to + * each module, returning the URL string. + * + * ```js + * const dependencyAsset = import.meta.resolve('component-lib/asset.css'); + * // file:///app/node_modules/component-lib/asset.css + * import.meta.resolve('./dep.js'); + * // file:///app/dep.js + * ``` + * + * All features of the Node.js module resolution are supported. Dependency + * resolutions are subject to the permitted exports resolutions within the package. + * + * **Caveats**: + * + * * This can result in synchronous file-system operations, which + * can impact performance similarly to `require.resolve`. + * * This feature is not available within custom loaders (it would + * create a deadlock). + * @since v13.9.0, v12.16.0 + * @param specifier The module specifier to resolve relative to the + * current module. + * @param parent An optional absolute parent module URL to resolve from. + * **Default:** `import.meta.url` + * @returns The absolute URL string that the specifier would resolve to. + */ + resolve(specifier: string, parent?: string | URL): string; + /** + * `true` when the current module is the entry point of the current process; `false` otherwise. + * + * Equivalent to `require.main === module` in CommonJS. + * + * Analogous to Python's `__name__ == "__main__"`. + * + * ```js + * export function foo() { + * return 'Hello, world'; + * } + * + * function main() { + * const message = foo(); + * console.log(message); + * } + * + * if (import.meta.main) main(); + * // `foo` can be imported from another module without possible side-effects from `main` + * ``` + * @since v22.18.0 + * @experimental + */ + main: boolean; + } + namespace NodeJS { + interface Module { + /** + * The module objects required for the first time by this one. + * @since v0.1.16 + */ + children: Module[]; + /** + * The `module.exports` object is created by the `Module` system. Sometimes this is + * not acceptable; many want their module to be an instance of some class. To do + * this, assign the desired export object to `module.exports`. + * @since v0.1.16 + */ + exports: any; + /** + * The fully resolved filename of the module. + * @since v0.1.16 + */ + filename: string; + /** + * The identifier for the module. Typically this is the fully resolved + * filename. + * @since v0.1.16 + */ + id: string; + /** + * `true` if the module is running during the Node.js preload + * phase. + * @since v15.4.0, v14.17.0 + */ + isPreloading: boolean; + /** + * Whether or not the module is done loading, or is in the process of + * loading. + * @since v0.1.16 + */ + loaded: boolean; + /** + * The module that first required this one, or `null` if the current module is the + * entry point of the current process, or `undefined` if the module was loaded by + * something that is not a CommonJS module (e.g. REPL or `import`). + * @since v0.1.16 + * @deprecated Please use `require.main` and `module.children` instead. + */ + parent: Module | null | undefined; + /** + * The directory name of the module. This is usually the same as the + * `path.dirname()` of the `module.id`. + * @since v11.14.0 + */ + path: string; + /** + * The search paths for the module. + * @since v0.4.0 + */ + paths: string[]; + /** + * The `module.require()` method provides a way to load a module as if + * `require()` was called from the original module. + * @since v0.5.1 + */ + require(id: string): any; + } + interface Require { + /** + * Used to import modules, `JSON`, and local files. + * @since v0.1.13 + */ + (id: string): any; + /** + * Modules are cached in this object when they are required. By deleting a key + * value from this object, the next `require` will reload the module. + * This does not apply to + * [native addons](https://nodejs.org/docs/latest-v22.x/api/addons.html), + * for which reloading will result in an error. + * @since v0.3.0 + */ + cache: Dict; + /** + * Instruct `require` on how to handle certain file extensions. + * @since v0.3.0 + * @deprecated + */ + extensions: RequireExtensions; + /** + * The `Module` object representing the entry script loaded when the Node.js + * process launched, or `undefined` if the entry point of the program is not a + * CommonJS module. + * @since v0.1.17 + */ + main: Module | undefined; + /** + * @since v0.3.0 + */ + resolve: RequireResolve; + } + /** @deprecated */ + interface RequireExtensions extends Dict<(module: Module, filename: string) => any> { + ".js": (module: Module, filename: string) => any; + ".json": (module: Module, filename: string) => any; + ".node": (module: Module, filename: string) => any; + } + interface RequireResolveOptions { + /** + * Paths to resolve module location from. If present, these + * paths are used instead of the default resolution paths, with the exception + * of + * [GLOBAL\_FOLDERS](https://nodejs.org/docs/latest-v22.x/api/modules.html#loading-from-the-global-folders) + * like `$HOME/.node_modules`, which are + * always included. Each of these paths is used as a starting point for + * the module resolution algorithm, meaning that the `node_modules` hierarchy + * is checked from this location. + * @since v8.9.0 + */ + paths?: string[] | undefined; + } + interface RequireResolve { + /** + * Use the internal `require()` machinery to look up the location of a module, + * but rather than loading the module, just return the resolved filename. + * + * If the module can not be found, a `MODULE_NOT_FOUND` error is thrown. + * @since v0.3.0 + * @param request The module path to resolve. + */ + (request: string, options?: RequireResolveOptions): string; + /** + * Returns an array containing the paths searched during resolution of `request` or + * `null` if the `request` string references a core module, for example `http` or + * `fs`. + * @since v8.9.0 + * @param request The module path whose lookup paths are being retrieved. + */ + paths(request: string): string[] | null; + } + } + /** + * The directory name of the current module. This is the same as the + * `path.dirname()` of the `__filename`. + * @since v0.1.27 + */ + var __dirname: string; + /** + * The file name of the current module. This is the current module file's absolute + * path with symlinks resolved. + * + * For a main program this is not necessarily the same as the file name used in the + * command line. + * @since v0.0.1 + */ + var __filename: string; + /** + * The `exports` variable is available within a module's file-level scope, and is + * assigned the value of `module.exports` before the module is evaluated. + * @since v0.1.16 + */ + var exports: NodeJS.Module["exports"]; + /** + * A reference to the current module. + * @since v0.1.16 + */ + var module: NodeJS.Module; + /** + * @since v0.1.13 + */ + var require: NodeJS.Require; + // Global-scope aliases for backwards compatibility with @types/node <13.0.x + /** @deprecated Use `NodeJS.Module` instead. */ + interface NodeModule extends NodeJS.Module {} + /** @deprecated Use `NodeJS.Require` instead. */ + interface NodeRequire extends NodeJS.Require {} + /** @deprecated Use `NodeJS.RequireResolve` instead. */ + interface RequireResolve extends NodeJS.RequireResolve {} + } + export = Module; +} +declare module "node:module" { + import module = require("module"); + export = module; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/net.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/net.d.ts new file mode 100644 index 00000000..d29b929a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/net.d.ts @@ -0,0 +1,1057 @@ +/** + * > Stability: 2 - Stable + * + * The `node:net` module provides an asynchronous network API for creating stream-based + * TCP or `IPC` servers ({@link createServer}) and clients + * ({@link createConnection}). + * + * It can be accessed using: + * + * ```js + * import net from 'node:net'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/net.js) + */ +declare module "net" { + import { NonSharedBuffer } from "node:buffer"; + import * as stream from "node:stream"; + import { Abortable, EventEmitter } from "node:events"; + import * as dns from "node:dns"; + type LookupFunction = ( + hostname: string, + options: dns.LookupOptions, + callback: (err: NodeJS.ErrnoException | null, address: string | dns.LookupAddress[], family?: number) => void, + ) => void; + interface AddressInfo { + address: string; + family: string; + port: number; + } + interface SocketConstructorOpts { + fd?: number | undefined; + allowHalfOpen?: boolean | undefined; + onread?: OnReadOpts | undefined; + readable?: boolean | undefined; + writable?: boolean | undefined; + signal?: AbortSignal | undefined; + } + interface OnReadOpts { + buffer: Uint8Array | (() => Uint8Array); + /** + * This function is called for every chunk of incoming data. + * Two arguments are passed to it: the number of bytes written to `buffer` and a reference to `buffer`. + * Return `false` from this function to implicitly `pause()` the socket. + */ + callback(bytesWritten: number, buffer: Uint8Array): boolean; + } + // TODO: remove empty ConnectOpts placeholder at next major @types/node version. + /** @deprecated */ + interface ConnectOpts {} + interface TcpSocketConnectOpts { + port: number; + host?: string | undefined; + localAddress?: string | undefined; + localPort?: number | undefined; + hints?: number | undefined; + family?: number | undefined; + lookup?: LookupFunction | undefined; + noDelay?: boolean | undefined; + keepAlive?: boolean | undefined; + keepAliveInitialDelay?: number | undefined; + /** + * @since v18.13.0 + */ + autoSelectFamily?: boolean | undefined; + /** + * @since v18.13.0 + */ + autoSelectFamilyAttemptTimeout?: number | undefined; + blockList?: BlockList | undefined; + } + interface IpcSocketConnectOpts { + path: string; + } + type SocketConnectOpts = TcpSocketConnectOpts | IpcSocketConnectOpts; + type SocketReadyState = "opening" | "open" | "readOnly" | "writeOnly" | "closed"; + /** + * This class is an abstraction of a TCP socket or a streaming `IPC` endpoint + * (uses named pipes on Windows, and Unix domain sockets otherwise). It is also + * an `EventEmitter`. + * + * A `net.Socket` can be created by the user and used directly to interact with + * a server. For example, it is returned by {@link createConnection}, + * so the user can use it to talk to the server. + * + * It can also be created by Node.js and passed to the user when a connection + * is received. For example, it is passed to the listeners of a `'connection'` event emitted on a {@link Server}, so the user can use + * it to interact with the client. + * @since v0.3.4 + */ + class Socket extends stream.Duplex { + constructor(options?: SocketConstructorOpts); + /** + * Destroys the socket after all data is written. If the `finish` event was already emitted the socket is destroyed immediately. + * If the socket is still writable it implicitly calls `socket.end()`. + * @since v0.3.4 + */ + destroySoon(): void; + /** + * Sends data on the socket. The second parameter specifies the encoding in the + * case of a string. It defaults to UTF8 encoding. + * + * Returns `true` if the entire data was flushed successfully to the kernel + * buffer. Returns `false` if all or part of the data was queued in user memory.`'drain'` will be emitted when the buffer is again free. + * + * The optional `callback` parameter will be executed when the data is finally + * written out, which may not be immediately. + * + * See `Writable` stream `write()` method for more + * information. + * @since v0.1.90 + * @param [encoding='utf8'] Only used when data is `string`. + */ + write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean; + write(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean; + /** + * Initiate a connection on a given socket. + * + * Possible signatures: + * + * * `socket.connect(options[, connectListener])` + * * `socket.connect(path[, connectListener])` for `IPC` connections. + * * `socket.connect(port[, host][, connectListener])` for TCP connections. + * * Returns: `net.Socket` The socket itself. + * + * This function is asynchronous. When the connection is established, the `'connect'` event will be emitted. If there is a problem connecting, + * instead of a `'connect'` event, an `'error'` event will be emitted with + * the error passed to the `'error'` listener. + * The last parameter `connectListener`, if supplied, will be added as a listener + * for the `'connect'` event **once**. + * + * This function should only be used for reconnecting a socket after`'close'` has been emitted or otherwise it may lead to undefined + * behavior. + */ + connect(options: SocketConnectOpts, connectionListener?: () => void): this; + connect(port: number, host: string, connectionListener?: () => void): this; + connect(port: number, connectionListener?: () => void): this; + connect(path: string, connectionListener?: () => void): this; + /** + * Set the encoding for the socket as a `Readable Stream`. See `readable.setEncoding()` for more information. + * @since v0.1.90 + * @return The socket itself. + */ + setEncoding(encoding?: BufferEncoding): this; + /** + * Pauses the reading of data. That is, `'data'` events will not be emitted. + * Useful to throttle back an upload. + * @return The socket itself. + */ + pause(): this; + /** + * Close the TCP connection by sending an RST packet and destroy the stream. + * If this TCP socket is in connecting status, it will send an RST packet and destroy this TCP socket once it is connected. + * Otherwise, it will call `socket.destroy` with an `ERR_SOCKET_CLOSED` Error. + * If this is not a TCP socket (for example, a pipe), calling this method will immediately throw an `ERR_INVALID_HANDLE_TYPE` Error. + * @since v18.3.0, v16.17.0 + */ + resetAndDestroy(): this; + /** + * Resumes reading after a call to `socket.pause()`. + * @return The socket itself. + */ + resume(): this; + /** + * Sets the socket to timeout after `timeout` milliseconds of inactivity on + * the socket. By default `net.Socket` do not have a timeout. + * + * When an idle timeout is triggered the socket will receive a `'timeout'` event but the connection will not be severed. The user must manually call `socket.end()` or `socket.destroy()` to + * end the connection. + * + * ```js + * socket.setTimeout(3000); + * socket.on('timeout', () => { + * console.log('socket timeout'); + * socket.end(); + * }); + * ``` + * + * If `timeout` is 0, then the existing idle timeout is disabled. + * + * The optional `callback` parameter will be added as a one-time listener for the `'timeout'` event. + * @since v0.1.90 + * @return The socket itself. + */ + setTimeout(timeout: number, callback?: () => void): this; + /** + * Enable/disable the use of Nagle's algorithm. + * + * When a TCP connection is created, it will have Nagle's algorithm enabled. + * + * Nagle's algorithm delays data before it is sent via the network. It attempts + * to optimize throughput at the expense of latency. + * + * Passing `true` for `noDelay` or not passing an argument will disable Nagle's + * algorithm for the socket. Passing `false` for `noDelay` will enable Nagle's + * algorithm. + * @since v0.1.90 + * @param [noDelay=true] + * @return The socket itself. + */ + setNoDelay(noDelay?: boolean): this; + /** + * Enable/disable keep-alive functionality, and optionally set the initial + * delay before the first keepalive probe is sent on an idle socket. + * + * Set `initialDelay` (in milliseconds) to set the delay between the last + * data packet received and the first keepalive probe. Setting `0` for`initialDelay` will leave the value unchanged from the default + * (or previous) setting. + * + * Enabling the keep-alive functionality will set the following socket options: + * + * * `SO_KEEPALIVE=1` + * * `TCP_KEEPIDLE=initialDelay` + * * `TCP_KEEPCNT=10` + * * `TCP_KEEPINTVL=1` + * @since v0.1.92 + * @param [enable=false] + * @param [initialDelay=0] + * @return The socket itself. + */ + setKeepAlive(enable?: boolean, initialDelay?: number): this; + /** + * Returns the bound `address`, the address `family` name and `port` of the + * socket as reported by the operating system:`{ port: 12346, family: 'IPv4', address: '127.0.0.1' }` + * @since v0.1.90 + */ + address(): AddressInfo | {}; + /** + * Calling `unref()` on a socket will allow the program to exit if this is the only + * active socket in the event system. If the socket is already `unref`ed calling`unref()` again will have no effect. + * @since v0.9.1 + * @return The socket itself. + */ + unref(): this; + /** + * Opposite of `unref()`, calling `ref()` on a previously `unref`ed socket will _not_ let the program exit if it's the only socket left (the default behavior). + * If the socket is `ref`ed calling `ref` again will have no effect. + * @since v0.9.1 + * @return The socket itself. + */ + ref(): this; + /** + * This property is only present if the family autoselection algorithm is enabled in `socket.connect(options)` + * and it is an array of the addresses that have been attempted. + * + * Each address is a string in the form of `$IP:$PORT`. + * If the connection was successful, then the last address is the one that the socket is currently connected to. + * @since v19.4.0 + */ + readonly autoSelectFamilyAttemptedAddresses: string[]; + /** + * This property shows the number of characters buffered for writing. The buffer + * may contain strings whose length after encoding is not yet known. So this number + * is only an approximation of the number of bytes in the buffer. + * + * `net.Socket` has the property that `socket.write()` always works. This is to + * help users get up and running quickly. The computer cannot always keep up + * with the amount of data that is written to a socket. The network connection + * simply might be too slow. Node.js will internally queue up the data written to a + * socket and send it out over the wire when it is possible. + * + * The consequence of this internal buffering is that memory may grow. + * Users who experience large or growing `bufferSize` should attempt to + * "throttle" the data flows in their program with `socket.pause()` and `socket.resume()`. + * @since v0.3.8 + * @deprecated Since v14.6.0 - Use `writableLength` instead. + */ + readonly bufferSize: number; + /** + * The amount of received bytes. + * @since v0.5.3 + */ + readonly bytesRead: number; + /** + * The amount of bytes sent. + * @since v0.5.3 + */ + readonly bytesWritten: number; + /** + * If `true`, `socket.connect(options[, connectListener])` was + * called and has not yet finished. It will stay `true` until the socket becomes + * connected, then it is set to `false` and the `'connect'` event is emitted. Note + * that the `socket.connect(options[, connectListener])` callback is a listener for the `'connect'` event. + * @since v6.1.0 + */ + readonly connecting: boolean; + /** + * This is `true` if the socket is not connected yet, either because `.connect()`has not yet been called or because it is still in the process of connecting + * (see `socket.connecting`). + * @since v11.2.0, v10.16.0 + */ + readonly pending: boolean; + /** + * See `writable.destroyed` for further details. + */ + readonly destroyed: boolean; + /** + * The string representation of the local IP address the remote client is + * connecting on. For example, in a server listening on `'0.0.0.0'`, if a client + * connects on `'192.168.1.1'`, the value of `socket.localAddress` would be`'192.168.1.1'`. + * @since v0.9.6 + */ + readonly localAddress?: string; + /** + * The numeric representation of the local port. For example, `80` or `21`. + * @since v0.9.6 + */ + readonly localPort?: number; + /** + * The string representation of the local IP family. `'IPv4'` or `'IPv6'`. + * @since v18.8.0, v16.18.0 + */ + readonly localFamily?: string; + /** + * This property represents the state of the connection as a string. + * + * * If the stream is connecting `socket.readyState` is `opening`. + * * If the stream is readable and writable, it is `open`. + * * If the stream is readable and not writable, it is `readOnly`. + * * If the stream is not readable and writable, it is `writeOnly`. + * @since v0.5.0 + */ + readonly readyState: SocketReadyState; + /** + * The string representation of the remote IP address. For example,`'74.125.127.100'` or `'2001:4860:a005::68'`. Value may be `undefined` if + * the socket is destroyed (for example, if the client disconnected). + * @since v0.5.10 + */ + readonly remoteAddress: string | undefined; + /** + * The string representation of the remote IP family. `'IPv4'` or `'IPv6'`. Value may be `undefined` if + * the socket is destroyed (for example, if the client disconnected). + * @since v0.11.14 + */ + readonly remoteFamily: string | undefined; + /** + * The numeric representation of the remote port. For example, `80` or `21`. Value may be `undefined` if + * the socket is destroyed (for example, if the client disconnected). + * @since v0.5.10 + */ + readonly remotePort: number | undefined; + /** + * The socket timeout in milliseconds as set by `socket.setTimeout()`. + * It is `undefined` if a timeout has not been set. + * @since v10.7.0 + */ + readonly timeout?: number; + /** + * Half-closes the socket. i.e., it sends a FIN packet. It is possible the + * server will still send some data. + * + * See `writable.end()` for further details. + * @since v0.1.90 + * @param [encoding='utf8'] Only used when data is `string`. + * @param callback Optional callback for when the socket is finished. + * @return The socket itself. + */ + end(callback?: () => void): this; + end(buffer: Uint8Array | string, callback?: () => void): this; + end(str: Uint8Array | string, encoding?: BufferEncoding, callback?: () => void): this; + /** + * events.EventEmitter + * 1. close + * 2. connect + * 3. connectionAttempt + * 4. connectionAttemptFailed + * 5. connectionAttemptTimeout + * 6. data + * 7. drain + * 8. end + * 9. error + * 10. lookup + * 11. ready + * 12. timeout + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "close", listener: (hadError: boolean) => void): this; + addListener(event: "connect", listener: () => void): this; + addListener(event: "connectionAttempt", listener: (ip: string, port: number, family: number) => void): this; + addListener( + event: "connectionAttemptFailed", + listener: (ip: string, port: number, family: number, error: Error) => void, + ): this; + addListener( + event: "connectionAttemptTimeout", + listener: (ip: string, port: number, family: number) => void, + ): this; + addListener(event: "data", listener: (data: NonSharedBuffer) => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "end", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener( + event: "lookup", + listener: (err: Error, address: string, family: string | number, host: string) => void, + ): this; + addListener(event: "ready", listener: () => void): this; + addListener(event: "timeout", listener: () => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "close", hadError: boolean): boolean; + emit(event: "connect"): boolean; + emit(event: "connectionAttempt", ip: string, port: number, family: number): boolean; + emit(event: "connectionAttemptFailed", ip: string, port: number, family: number, error: Error): boolean; + emit(event: "connectionAttemptTimeout", ip: string, port: number, family: number): boolean; + emit(event: "data", data: NonSharedBuffer): boolean; + emit(event: "drain"): boolean; + emit(event: "end"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "lookup", err: Error, address: string, family: string | number, host: string): boolean; + emit(event: "ready"): boolean; + emit(event: "timeout"): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "close", listener: (hadError: boolean) => void): this; + on(event: "connect", listener: () => void): this; + on(event: "connectionAttempt", listener: (ip: string, port: number, family: number) => void): this; + on( + event: "connectionAttemptFailed", + listener: (ip: string, port: number, family: number, error: Error) => void, + ): this; + on(event: "connectionAttemptTimeout", listener: (ip: string, port: number, family: number) => void): this; + on(event: "data", listener: (data: NonSharedBuffer) => void): this; + on(event: "drain", listener: () => void): this; + on(event: "end", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on( + event: "lookup", + listener: (err: Error, address: string, family: string | number, host: string) => void, + ): this; + on(event: "ready", listener: () => void): this; + on(event: "timeout", listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "close", listener: (hadError: boolean) => void): this; + once(event: "connectionAttempt", listener: (ip: string, port: number, family: number) => void): this; + once( + event: "connectionAttemptFailed", + listener: (ip: string, port: number, family: number, error: Error) => void, + ): this; + once(event: "connectionAttemptTimeout", listener: (ip: string, port: number, family: number) => void): this; + once(event: "connect", listener: () => void): this; + once(event: "data", listener: (data: NonSharedBuffer) => void): this; + once(event: "drain", listener: () => void): this; + once(event: "end", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once( + event: "lookup", + listener: (err: Error, address: string, family: string | number, host: string) => void, + ): this; + once(event: "ready", listener: () => void): this; + once(event: "timeout", listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: (hadError: boolean) => void): this; + prependListener(event: "connect", listener: () => void): this; + prependListener(event: "connectionAttempt", listener: (ip: string, port: number, family: number) => void): this; + prependListener( + event: "connectionAttemptFailed", + listener: (ip: string, port: number, family: number, error: Error) => void, + ): this; + prependListener( + event: "connectionAttemptTimeout", + listener: (ip: string, port: number, family: number) => void, + ): this; + prependListener(event: "data", listener: (data: NonSharedBuffer) => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "end", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener( + event: "lookup", + listener: (err: Error, address: string, family: string | number, host: string) => void, + ): this; + prependListener(event: "ready", listener: () => void): this; + prependListener(event: "timeout", listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: (hadError: boolean) => void): this; + prependOnceListener(event: "connect", listener: () => void): this; + prependOnceListener( + event: "connectionAttempt", + listener: (ip: string, port: number, family: number) => void, + ): this; + prependOnceListener( + event: "connectionAttemptFailed", + listener: (ip: string, port: number, family: number, error: Error) => void, + ): this; + prependOnceListener( + event: "connectionAttemptTimeout", + listener: (ip: string, port: number, family: number) => void, + ): this; + prependOnceListener(event: "data", listener: (data: NonSharedBuffer) => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "end", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener( + event: "lookup", + listener: (err: Error, address: string, family: string | number, host: string) => void, + ): this; + prependOnceListener(event: "ready", listener: () => void): this; + prependOnceListener(event: "timeout", listener: () => void): this; + } + interface ListenOptions extends Abortable { + backlog?: number | undefined; + exclusive?: boolean | undefined; + host?: string | undefined; + /** + * @default false + */ + ipv6Only?: boolean | undefined; + reusePort?: boolean | undefined; + path?: string | undefined; + port?: number | undefined; + readableAll?: boolean | undefined; + writableAll?: boolean | undefined; + } + interface ServerOpts { + /** + * Indicates whether half-opened TCP connections are allowed. + * @default false + */ + allowHalfOpen?: boolean | undefined; + /** + * Indicates whether the socket should be paused on incoming connections. + * @default false + */ + pauseOnConnect?: boolean | undefined; + /** + * If set to `true`, it disables the use of Nagle's algorithm immediately after a new incoming connection is received. + * @default false + * @since v16.5.0 + */ + noDelay?: boolean | undefined; + /** + * If set to `true`, it enables keep-alive functionality on the socket immediately after a new incoming connection is received, + * similarly on what is done in `socket.setKeepAlive([enable][, initialDelay])`. + * @default false + * @since v16.5.0 + */ + keepAlive?: boolean | undefined; + /** + * If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket. + * @default 0 + * @since v16.5.0 + */ + keepAliveInitialDelay?: number | undefined; + /** + * Optionally overrides all `net.Socket`s' `readableHighWaterMark` and `writableHighWaterMark`. + * @default See [stream.getDefaultHighWaterMark()](https://nodejs.org/docs/latest-v22.x/api/stream.html#streamgetdefaulthighwatermarkobjectmode). + * @since v18.17.0, v20.1.0 + */ + highWaterMark?: number | undefined; + /** + * `blockList` can be used for disabling inbound + * access to specific IP addresses, IP ranges, or IP subnets. This does not + * work if the server is behind a reverse proxy, NAT, etc. because the address + * checked against the block list is the address of the proxy, or the one + * specified by the NAT. + * @since v22.13.0 + */ + blockList?: BlockList | undefined; + } + interface DropArgument { + localAddress?: string; + localPort?: number; + localFamily?: string; + remoteAddress?: string; + remotePort?: number; + remoteFamily?: string; + } + /** + * This class is used to create a TCP or `IPC` server. + * @since v0.1.90 + */ + class Server extends EventEmitter { + constructor(connectionListener?: (socket: Socket) => void); + constructor(options?: ServerOpts, connectionListener?: (socket: Socket) => void); + /** + * Start a server listening for connections. A `net.Server` can be a TCP or + * an `IPC` server depending on what it listens to. + * + * Possible signatures: + * + * * `server.listen(handle[, backlog][, callback])` + * * `server.listen(options[, callback])` + * * `server.listen(path[, backlog][, callback])` for `IPC` servers + * * `server.listen([port[, host[, backlog]]][, callback])` for TCP servers + * + * This function is asynchronous. When the server starts listening, the `'listening'` event will be emitted. The last parameter `callback`will be added as a listener for the `'listening'` + * event. + * + * All `listen()` methods can take a `backlog` parameter to specify the maximum + * length of the queue of pending connections. The actual length will be determined + * by the OS through sysctl settings such as `tcp_max_syn_backlog` and `somaxconn` on Linux. The default value of this parameter is 511 (not 512). + * + * All {@link Socket} are set to `SO_REUSEADDR` (see [`socket(7)`](https://man7.org/linux/man-pages/man7/socket.7.html) for + * details). + * + * The `server.listen()` method can be called again if and only if there was an + * error during the first `server.listen()` call or `server.close()` has been + * called. Otherwise, an `ERR_SERVER_ALREADY_LISTEN` error will be thrown. + * + * One of the most common errors raised when listening is `EADDRINUSE`. + * This happens when another server is already listening on the requested`port`/`path`/`handle`. One way to handle this would be to retry + * after a certain amount of time: + * + * ```js + * server.on('error', (e) => { + * if (e.code === 'EADDRINUSE') { + * console.error('Address in use, retrying...'); + * setTimeout(() => { + * server.close(); + * server.listen(PORT, HOST); + * }, 1000); + * } + * }); + * ``` + */ + listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this; + listen(port?: number, hostname?: string, listeningListener?: () => void): this; + listen(port?: number, backlog?: number, listeningListener?: () => void): this; + listen(port?: number, listeningListener?: () => void): this; + listen(path: string, backlog?: number, listeningListener?: () => void): this; + listen(path: string, listeningListener?: () => void): this; + listen(options: ListenOptions, listeningListener?: () => void): this; + listen(handle: any, backlog?: number, listeningListener?: () => void): this; + listen(handle: any, listeningListener?: () => void): this; + /** + * Stops the server from accepting new connections and keeps existing + * connections. This function is asynchronous, the server is finally closed + * when all connections are ended and the server emits a `'close'` event. + * The optional `callback` will be called once the `'close'` event occurs. Unlike + * that event, it will be called with an `Error` as its only argument if the server + * was not open when it was closed. + * @since v0.1.90 + * @param callback Called when the server is closed. + */ + close(callback?: (err?: Error) => void): this; + /** + * Returns the bound `address`, the address `family` name, and `port` of the server + * as reported by the operating system if listening on an IP socket + * (useful to find which port was assigned when getting an OS-assigned address):`{ port: 12346, family: 'IPv4', address: '127.0.0.1' }`. + * + * For a server listening on a pipe or Unix domain socket, the name is returned + * as a string. + * + * ```js + * const server = net.createServer((socket) => { + * socket.end('goodbye\n'); + * }).on('error', (err) => { + * // Handle errors here. + * throw err; + * }); + * + * // Grab an arbitrary unused port. + * server.listen(() => { + * console.log('opened server on', server.address()); + * }); + * ``` + * + * `server.address()` returns `null` before the `'listening'` event has been + * emitted or after calling `server.close()`. + * @since v0.1.90 + */ + address(): AddressInfo | string | null; + /** + * Asynchronously get the number of concurrent connections on the server. Works + * when sockets were sent to forks. + * + * Callback should take two arguments `err` and `count`. + * @since v0.9.7 + */ + getConnections(cb: (error: Error | null, count: number) => void): this; + /** + * Opposite of `unref()`, calling `ref()` on a previously `unref`ed server will _not_ let the program exit if it's the only server left (the default behavior). + * If the server is `ref`ed calling `ref()` again will have no effect. + * @since v0.9.1 + */ + ref(): this; + /** + * Calling `unref()` on a server will allow the program to exit if this is the only + * active server in the event system. If the server is already `unref`ed calling`unref()` again will have no effect. + * @since v0.9.1 + */ + unref(): this; + /** + * Set this property to reject connections when the server's connection count gets + * high. + * + * It is not recommended to use this option once a socket has been sent to a child + * with `child_process.fork()`. + * @since v0.2.0 + */ + maxConnections: number; + connections: number; + /** + * Indicates whether or not the server is listening for connections. + * @since v5.7.0 + */ + readonly listening: boolean; + /** + * events.EventEmitter + * 1. close + * 2. connection + * 3. error + * 4. listening + * 5. drop + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "connection", listener: (socket: Socket) => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "listening", listener: () => void): this; + addListener(event: "drop", listener: (data?: DropArgument) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "close"): boolean; + emit(event: "connection", socket: Socket): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "listening"): boolean; + emit(event: "drop", data?: DropArgument): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "close", listener: () => void): this; + on(event: "connection", listener: (socket: Socket) => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "listening", listener: () => void): this; + on(event: "drop", listener: (data?: DropArgument) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "connection", listener: (socket: Socket) => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "listening", listener: () => void): this; + once(event: "drop", listener: (data?: DropArgument) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "connection", listener: (socket: Socket) => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "listening", listener: () => void): this; + prependListener(event: "drop", listener: (data?: DropArgument) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "connection", listener: (socket: Socket) => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "listening", listener: () => void): this; + prependOnceListener(event: "drop", listener: (data?: DropArgument) => void): this; + /** + * Calls {@link Server.close()} and returns a promise that fulfills when the server has closed. + * @since v20.5.0 + */ + [Symbol.asyncDispose](): Promise; + } + type IPVersion = "ipv4" | "ipv6"; + /** + * The `BlockList` object can be used with some network APIs to specify rules for + * disabling inbound or outbound access to specific IP addresses, IP ranges, or + * IP subnets. + * @since v15.0.0, v14.18.0 + */ + class BlockList { + /** + * Adds a rule to block the given IP address. + * @since v15.0.0, v14.18.0 + * @param address An IPv4 or IPv6 address. + * @param [type='ipv4'] Either `'ipv4'` or `'ipv6'`. + */ + addAddress(address: string, type?: IPVersion): void; + addAddress(address: SocketAddress): void; + /** + * Adds a rule to block a range of IP addresses from `start` (inclusive) to`end` (inclusive). + * @since v15.0.0, v14.18.0 + * @param start The starting IPv4 or IPv6 address in the range. + * @param end The ending IPv4 or IPv6 address in the range. + * @param [type='ipv4'] Either `'ipv4'` or `'ipv6'`. + */ + addRange(start: string, end: string, type?: IPVersion): void; + addRange(start: SocketAddress, end: SocketAddress): void; + /** + * Adds a rule to block a range of IP addresses specified as a subnet mask. + * @since v15.0.0, v14.18.0 + * @param net The network IPv4 or IPv6 address. + * @param prefix The number of CIDR prefix bits. For IPv4, this must be a value between `0` and `32`. For IPv6, this must be between `0` and `128`. + * @param [type='ipv4'] Either `'ipv4'` or `'ipv6'`. + */ + addSubnet(net: SocketAddress, prefix: number): void; + addSubnet(net: string, prefix: number, type?: IPVersion): void; + /** + * Returns `true` if the given IP address matches any of the rules added to the`BlockList`. + * + * ```js + * const blockList = new net.BlockList(); + * blockList.addAddress('123.123.123.123'); + * blockList.addRange('10.0.0.1', '10.0.0.10'); + * blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6'); + * + * console.log(blockList.check('123.123.123.123')); // Prints: true + * console.log(blockList.check('10.0.0.3')); // Prints: true + * console.log(blockList.check('222.111.111.222')); // Prints: false + * + * // IPv6 notation for IPv4 addresses works: + * console.log(blockList.check('::ffff:7b7b:7b7b', 'ipv6')); // Prints: true + * console.log(blockList.check('::ffff:123.123.123.123', 'ipv6')); // Prints: true + * ``` + * @since v15.0.0, v14.18.0 + * @param address The IP address to check + * @param [type='ipv4'] Either `'ipv4'` or `'ipv6'`. + */ + check(address: SocketAddress): boolean; + check(address: string, type?: IPVersion): boolean; + /** + * The list of rules added to the blocklist. + * @since v15.0.0, v14.18.0 + */ + rules: readonly string[]; + /** + * Returns `true` if the `value` is a `net.BlockList`. + * @since v22.13.0 + * @param value Any JS value + */ + static isBlockList(value: unknown): value is BlockList; + /** + * ```js + * const blockList = new net.BlockList(); + * const data = [ + * 'Subnet: IPv4 192.168.1.0/24', + * 'Address: IPv4 10.0.0.5', + * 'Range: IPv4 192.168.2.1-192.168.2.10', + * 'Range: IPv4 10.0.0.1-10.0.0.10', + * ]; + * blockList.fromJSON(data); + * blockList.fromJSON(JSON.stringify(data)); + * ``` + * @since v22.19.0 + * @experimental + */ + fromJSON(data: string | readonly string[]): void; + /** + * @since v22.19.0 + * @experimental + */ + toJSON(): readonly string[]; + } + interface TcpNetConnectOpts extends TcpSocketConnectOpts, SocketConstructorOpts { + timeout?: number | undefined; + } + interface IpcNetConnectOpts extends IpcSocketConnectOpts, SocketConstructorOpts { + timeout?: number | undefined; + } + type NetConnectOpts = TcpNetConnectOpts | IpcNetConnectOpts; + /** + * Creates a new TCP or `IPC` server. + * + * If `allowHalfOpen` is set to `true`, when the other end of the socket + * signals the end of transmission, the server will only send back the end of + * transmission when `socket.end()` is explicitly called. For example, in the + * context of TCP, when a FIN packed is received, a FIN packed is sent + * back only when `socket.end()` is explicitly called. Until then the + * connection is half-closed (non-readable but still writable). See `'end'` event and [RFC 1122](https://tools.ietf.org/html/rfc1122) (section 4.2.2.13) for more information. + * + * If `pauseOnConnect` is set to `true`, then the socket associated with each + * incoming connection will be paused, and no data will be read from its handle. + * This allows connections to be passed between processes without any data being + * read by the original process. To begin reading data from a paused socket, call `socket.resume()`. + * + * The server can be a TCP server or an `IPC` server, depending on what it `listen()` to. + * + * Here is an example of a TCP echo server which listens for connections + * on port 8124: + * + * ```js + * import net from 'node:net'; + * const server = net.createServer((c) => { + * // 'connection' listener. + * console.log('client connected'); + * c.on('end', () => { + * console.log('client disconnected'); + * }); + * c.write('hello\r\n'); + * c.pipe(c); + * }); + * server.on('error', (err) => { + * throw err; + * }); + * server.listen(8124, () => { + * console.log('server bound'); + * }); + * ``` + * + * Test this by using `telnet`: + * + * ```bash + * telnet localhost 8124 + * ``` + * + * To listen on the socket `/tmp/echo.sock`: + * + * ```js + * server.listen('/tmp/echo.sock', () => { + * console.log('server bound'); + * }); + * ``` + * + * Use `nc` to connect to a Unix domain socket server: + * + * ```bash + * nc -U /tmp/echo.sock + * ``` + * @since v0.5.0 + * @param connectionListener Automatically set as a listener for the {@link 'connection'} event. + */ + function createServer(connectionListener?: (socket: Socket) => void): Server; + function createServer(options?: ServerOpts, connectionListener?: (socket: Socket) => void): Server; + /** + * Aliases to {@link createConnection}. + * + * Possible signatures: + * + * * {@link connect} + * * {@link connect} for `IPC` connections. + * * {@link connect} for TCP connections. + */ + function connect(options: NetConnectOpts, connectionListener?: () => void): Socket; + function connect(port: number, host?: string, connectionListener?: () => void): Socket; + function connect(path: string, connectionListener?: () => void): Socket; + /** + * A factory function, which creates a new {@link Socket}, + * immediately initiates connection with `socket.connect()`, + * then returns the `net.Socket` that starts the connection. + * + * When the connection is established, a `'connect'` event will be emitted + * on the returned socket. The last parameter `connectListener`, if supplied, + * will be added as a listener for the `'connect'` event **once**. + * + * Possible signatures: + * + * * {@link createConnection} + * * {@link createConnection} for `IPC` connections. + * * {@link createConnection} for TCP connections. + * + * The {@link connect} function is an alias to this function. + */ + function createConnection(options: NetConnectOpts, connectionListener?: () => void): Socket; + function createConnection(port: number, host?: string, connectionListener?: () => void): Socket; + function createConnection(path: string, connectionListener?: () => void): Socket; + /** + * Gets the current default value of the `autoSelectFamily` option of `socket.connect(options)`. + * The initial default value is `true`, unless the command line option`--no-network-family-autoselection` is provided. + * @since v19.4.0 + */ + function getDefaultAutoSelectFamily(): boolean; + /** + * Sets the default value of the `autoSelectFamily` option of `socket.connect(options)`. + * @param value The new default value. + * The initial default value is `true`, unless the command line option + * `--no-network-family-autoselection` is provided. + * @since v19.4.0 + */ + function setDefaultAutoSelectFamily(value: boolean): void; + /** + * Gets the current default value of the `autoSelectFamilyAttemptTimeout` option of `socket.connect(options)`. + * The initial default value is `250` or the value specified via the command line option `--network-family-autoselection-attempt-timeout`. + * @returns The current default value of the `autoSelectFamilyAttemptTimeout` option. + * @since v19.8.0, v18.8.0 + */ + function getDefaultAutoSelectFamilyAttemptTimeout(): number; + /** + * Sets the default value of the `autoSelectFamilyAttemptTimeout` option of `socket.connect(options)`. + * @param value The new default value, which must be a positive number. If the number is less than `10`, the value `10` is used instead. The initial default value is `250` or the value specified via the command line + * option `--network-family-autoselection-attempt-timeout`. + * @since v19.8.0, v18.8.0 + */ + function setDefaultAutoSelectFamilyAttemptTimeout(value: number): void; + /** + * Returns `6` if `input` is an IPv6 address. Returns `4` if `input` is an IPv4 + * address in [dot-decimal notation](https://en.wikipedia.org/wiki/Dot-decimal_notation) with no leading zeroes. Otherwise, returns`0`. + * + * ```js + * net.isIP('::1'); // returns 6 + * net.isIP('127.0.0.1'); // returns 4 + * net.isIP('127.000.000.001'); // returns 0 + * net.isIP('127.0.0.1/24'); // returns 0 + * net.isIP('fhqwhgads'); // returns 0 + * ``` + * @since v0.3.0 + */ + function isIP(input: string): number; + /** + * Returns `true` if `input` is an IPv4 address in [dot-decimal notation](https://en.wikipedia.org/wiki/Dot-decimal_notation) with no + * leading zeroes. Otherwise, returns `false`. + * + * ```js + * net.isIPv4('127.0.0.1'); // returns true + * net.isIPv4('127.000.000.001'); // returns false + * net.isIPv4('127.0.0.1/24'); // returns false + * net.isIPv4('fhqwhgads'); // returns false + * ``` + * @since v0.3.0 + */ + function isIPv4(input: string): boolean; + /** + * Returns `true` if `input` is an IPv6 address. Otherwise, returns `false`. + * + * ```js + * net.isIPv6('::1'); // returns true + * net.isIPv6('fhqwhgads'); // returns false + * ``` + * @since v0.3.0 + */ + function isIPv6(input: string): boolean; + interface SocketAddressInitOptions { + /** + * The network address as either an IPv4 or IPv6 string. + * @default 127.0.0.1 + */ + address?: string | undefined; + /** + * @default `'ipv4'` + */ + family?: IPVersion | undefined; + /** + * An IPv6 flow-label used only if `family` is `'ipv6'`. + * @default 0 + */ + flowlabel?: number | undefined; + /** + * An IP port. + * @default 0 + */ + port?: number | undefined; + } + /** + * @since v15.14.0, v14.18.0 + */ + class SocketAddress { + constructor(options: SocketAddressInitOptions); + /** + * Either \`'ipv4'\` or \`'ipv6'\`. + * @since v15.14.0, v14.18.0 + */ + readonly address: string; + /** + * Either \`'ipv4'\` or \`'ipv6'\`. + * @since v15.14.0, v14.18.0 + */ + readonly family: IPVersion; + /** + * @since v15.14.0, v14.18.0 + */ + readonly port: number; + /** + * @since v15.14.0, v14.18.0 + */ + readonly flowlabel: number; + /** + * @since v22.13.0 + * @param input An input string containing an IP address and optional port, + * e.g. `123.1.2.3:1234` or `[1::1]:1234`. + * @returns Returns a `SocketAddress` if parsing was successful. + * Otherwise returns `undefined`. + */ + static parse(input: string): SocketAddress | undefined; + } +} +declare module "node:net" { + export * from "net"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/os.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/os.d.ts new file mode 100644 index 00000000..a40bd77b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/os.d.ts @@ -0,0 +1,506 @@ +/** + * The `node:os` module provides operating system-related utility methods and + * properties. It can be accessed using: + * + * ```js + * import os from 'node:os'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/os.js) + */ +declare module "os" { + import { NonSharedBuffer } from "buffer"; + interface CpuInfo { + model: string; + speed: number; + times: { + /** The number of milliseconds the CPU has spent in user mode. */ + user: number; + /** The number of milliseconds the CPU has spent in nice mode. */ + nice: number; + /** The number of milliseconds the CPU has spent in sys mode. */ + sys: number; + /** The number of milliseconds the CPU has spent in idle mode. */ + idle: number; + /** The number of milliseconds the CPU has spent in irq mode. */ + irq: number; + }; + } + interface NetworkInterfaceBase { + address: string; + netmask: string; + mac: string; + internal: boolean; + cidr: string | null; + scopeid?: number; + } + interface NetworkInterfaceInfoIPv4 extends NetworkInterfaceBase { + family: "IPv4"; + } + interface NetworkInterfaceInfoIPv6 extends NetworkInterfaceBase { + family: "IPv6"; + scopeid: number; + } + interface UserInfo { + username: T; + uid: number; + gid: number; + shell: T | null; + homedir: T; + } + type NetworkInterfaceInfo = NetworkInterfaceInfoIPv4 | NetworkInterfaceInfoIPv6; + /** + * Returns the host name of the operating system as a string. + * @since v0.3.3 + */ + function hostname(): string; + /** + * Returns an array containing the 1, 5, and 15 minute load averages. + * + * The load average is a measure of system activity calculated by the operating + * system and expressed as a fractional number. + * + * The load average is a Unix-specific concept. On Windows, the return value is + * always `[0, 0, 0]`. + * @since v0.3.3 + */ + function loadavg(): number[]; + /** + * Returns the system uptime in number of seconds. + * @since v0.3.3 + */ + function uptime(): number; + /** + * Returns the amount of free system memory in bytes as an integer. + * @since v0.3.3 + */ + function freemem(): number; + /** + * Returns the total amount of system memory in bytes as an integer. + * @since v0.3.3 + */ + function totalmem(): number; + /** + * Returns an array of objects containing information about each logical CPU core. + * The array will be empty if no CPU information is available, such as if the `/proc` file system is unavailable. + * + * The properties included on each object include: + * + * ```js + * [ + * { + * model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz', + * speed: 2926, + * times: { + * user: 252020, + * nice: 0, + * sys: 30340, + * idle: 1070356870, + * irq: 0, + * }, + * }, + * { + * model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz', + * speed: 2926, + * times: { + * user: 306960, + * nice: 0, + * sys: 26980, + * idle: 1071569080, + * irq: 0, + * }, + * }, + * { + * model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz', + * speed: 2926, + * times: { + * user: 248450, + * nice: 0, + * sys: 21750, + * idle: 1070919370, + * irq: 0, + * }, + * }, + * { + * model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz', + * speed: 2926, + * times: { + * user: 256880, + * nice: 0, + * sys: 19430, + * idle: 1070905480, + * irq: 20, + * }, + * }, + * ] + * ``` + * + * `nice` values are POSIX-only. On Windows, the `nice` values of all processors + * are always 0. + * + * `os.cpus().length` should not be used to calculate the amount of parallelism + * available to an application. Use {@link availableParallelism} for this purpose. + * @since v0.3.3 + */ + function cpus(): CpuInfo[]; + /** + * Returns an estimate of the default amount of parallelism a program should use. + * Always returns a value greater than zero. + * + * This function is a small wrapper about libuv's [`uv_available_parallelism()`](https://docs.libuv.org/en/v1.x/misc.html#c.uv_available_parallelism). + * @since v19.4.0, v18.14.0 + */ + function availableParallelism(): number; + /** + * Returns the operating system name as returned by [`uname(3)`](https://linux.die.net/man/3/uname). For example, it + * returns `'Linux'` on Linux, `'Darwin'` on macOS, and `'Windows_NT'` on Windows. + * + * See [https://en.wikipedia.org/wiki/Uname#Examples](https://en.wikipedia.org/wiki/Uname#Examples) for additional information + * about the output of running [`uname(3)`](https://linux.die.net/man/3/uname) on various operating systems. + * @since v0.3.3 + */ + function type(): string; + /** + * Returns the operating system as a string. + * + * On POSIX systems, the operating system release is determined by calling [`uname(3)`](https://linux.die.net/man/3/uname). On Windows, `GetVersionExW()` is used. See + * [https://en.wikipedia.org/wiki/Uname#Examples](https://en.wikipedia.org/wiki/Uname#Examples) for more information. + * @since v0.3.3 + */ + function release(): string; + /** + * Returns an object containing network interfaces that have been assigned a + * network address. + * + * Each key on the returned object identifies a network interface. The associated + * value is an array of objects that each describe an assigned network address. + * + * The properties available on the assigned network address object include: + * + * ```js + * { + * lo: [ + * { + * address: '127.0.0.1', + * netmask: '255.0.0.0', + * family: 'IPv4', + * mac: '00:00:00:00:00:00', + * internal: true, + * cidr: '127.0.0.1/8' + * }, + * { + * address: '::1', + * netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + * family: 'IPv6', + * mac: '00:00:00:00:00:00', + * scopeid: 0, + * internal: true, + * cidr: '::1/128' + * } + * ], + * eth0: [ + * { + * address: '192.168.1.108', + * netmask: '255.255.255.0', + * family: 'IPv4', + * mac: '01:02:03:0a:0b:0c', + * internal: false, + * cidr: '192.168.1.108/24' + * }, + * { + * address: 'fe80::a00:27ff:fe4e:66a1', + * netmask: 'ffff:ffff:ffff:ffff::', + * family: 'IPv6', + * mac: '01:02:03:0a:0b:0c', + * scopeid: 1, + * internal: false, + * cidr: 'fe80::a00:27ff:fe4e:66a1/64' + * } + * ] + * } + * ``` + * @since v0.6.0 + */ + function networkInterfaces(): NodeJS.Dict; + /** + * Returns the string path of the current user's home directory. + * + * On POSIX, it uses the `$HOME` environment variable if defined. Otherwise it + * uses the [effective UID](https://en.wikipedia.org/wiki/User_identifier#Effective_user_ID) to look up the user's home directory. + * + * On Windows, it uses the `USERPROFILE` environment variable if defined. + * Otherwise it uses the path to the profile directory of the current user. + * @since v2.3.0 + */ + function homedir(): string; + interface UserInfoOptions { + encoding?: BufferEncoding | "buffer" | undefined; + } + interface UserInfoOptionsWithBufferEncoding extends UserInfoOptions { + encoding: "buffer"; + } + interface UserInfoOptionsWithStringEncoding extends UserInfoOptions { + encoding?: BufferEncoding | undefined; + } + /** + * Returns information about the currently effective user. On POSIX platforms, + * this is typically a subset of the password file. The returned object includes + * the `username`, `uid`, `gid`, `shell`, and `homedir`. On Windows, the `uid` and `gid` fields are `-1`, and `shell` is `null`. + * + * The value of `homedir` returned by `os.userInfo()` is provided by the operating + * system. This differs from the result of `os.homedir()`, which queries + * environment variables for the home directory before falling back to the + * operating system response. + * + * Throws a [`SystemError`](https://nodejs.org/docs/latest-v22.x/api/errors.html#class-systemerror) if a user has no `username` or `homedir`. + * @since v6.0.0 + */ + function userInfo(options?: UserInfoOptionsWithStringEncoding): UserInfo; + function userInfo(options: UserInfoOptionsWithBufferEncoding): UserInfo; + function userInfo(options: UserInfoOptions): UserInfo; + type SignalConstants = { + [key in NodeJS.Signals]: number; + }; + namespace constants { + const UV_UDP_REUSEADDR: number; + namespace signals {} + const signals: SignalConstants; + namespace errno { + const E2BIG: number; + const EACCES: number; + const EADDRINUSE: number; + const EADDRNOTAVAIL: number; + const EAFNOSUPPORT: number; + const EAGAIN: number; + const EALREADY: number; + const EBADF: number; + const EBADMSG: number; + const EBUSY: number; + const ECANCELED: number; + const ECHILD: number; + const ECONNABORTED: number; + const ECONNREFUSED: number; + const ECONNRESET: number; + const EDEADLK: number; + const EDESTADDRREQ: number; + const EDOM: number; + const EDQUOT: number; + const EEXIST: number; + const EFAULT: number; + const EFBIG: number; + const EHOSTUNREACH: number; + const EIDRM: number; + const EILSEQ: number; + const EINPROGRESS: number; + const EINTR: number; + const EINVAL: number; + const EIO: number; + const EISCONN: number; + const EISDIR: number; + const ELOOP: number; + const EMFILE: number; + const EMLINK: number; + const EMSGSIZE: number; + const EMULTIHOP: number; + const ENAMETOOLONG: number; + const ENETDOWN: number; + const ENETRESET: number; + const ENETUNREACH: number; + const ENFILE: number; + const ENOBUFS: number; + const ENODATA: number; + const ENODEV: number; + const ENOENT: number; + const ENOEXEC: number; + const ENOLCK: number; + const ENOLINK: number; + const ENOMEM: number; + const ENOMSG: number; + const ENOPROTOOPT: number; + const ENOSPC: number; + const ENOSR: number; + const ENOSTR: number; + const ENOSYS: number; + const ENOTCONN: number; + const ENOTDIR: number; + const ENOTEMPTY: number; + const ENOTSOCK: number; + const ENOTSUP: number; + const ENOTTY: number; + const ENXIO: number; + const EOPNOTSUPP: number; + const EOVERFLOW: number; + const EPERM: number; + const EPIPE: number; + const EPROTO: number; + const EPROTONOSUPPORT: number; + const EPROTOTYPE: number; + const ERANGE: number; + const EROFS: number; + const ESPIPE: number; + const ESRCH: number; + const ESTALE: number; + const ETIME: number; + const ETIMEDOUT: number; + const ETXTBSY: number; + const EWOULDBLOCK: number; + const EXDEV: number; + const WSAEINTR: number; + const WSAEBADF: number; + const WSAEACCES: number; + const WSAEFAULT: number; + const WSAEINVAL: number; + const WSAEMFILE: number; + const WSAEWOULDBLOCK: number; + const WSAEINPROGRESS: number; + const WSAEALREADY: number; + const WSAENOTSOCK: number; + const WSAEDESTADDRREQ: number; + const WSAEMSGSIZE: number; + const WSAEPROTOTYPE: number; + const WSAENOPROTOOPT: number; + const WSAEPROTONOSUPPORT: number; + const WSAESOCKTNOSUPPORT: number; + const WSAEOPNOTSUPP: number; + const WSAEPFNOSUPPORT: number; + const WSAEAFNOSUPPORT: number; + const WSAEADDRINUSE: number; + const WSAEADDRNOTAVAIL: number; + const WSAENETDOWN: number; + const WSAENETUNREACH: number; + const WSAENETRESET: number; + const WSAECONNABORTED: number; + const WSAECONNRESET: number; + const WSAENOBUFS: number; + const WSAEISCONN: number; + const WSAENOTCONN: number; + const WSAESHUTDOWN: number; + const WSAETOOMANYREFS: number; + const WSAETIMEDOUT: number; + const WSAECONNREFUSED: number; + const WSAELOOP: number; + const WSAENAMETOOLONG: number; + const WSAEHOSTDOWN: number; + const WSAEHOSTUNREACH: number; + const WSAENOTEMPTY: number; + const WSAEPROCLIM: number; + const WSAEUSERS: number; + const WSAEDQUOT: number; + const WSAESTALE: number; + const WSAEREMOTE: number; + const WSASYSNOTREADY: number; + const WSAVERNOTSUPPORTED: number; + const WSANOTINITIALISED: number; + const WSAEDISCON: number; + const WSAENOMORE: number; + const WSAECANCELLED: number; + const WSAEINVALIDPROCTABLE: number; + const WSAEINVALIDPROVIDER: number; + const WSAEPROVIDERFAILEDINIT: number; + const WSASYSCALLFAILURE: number; + const WSASERVICE_NOT_FOUND: number; + const WSATYPE_NOT_FOUND: number; + const WSA_E_NO_MORE: number; + const WSA_E_CANCELLED: number; + const WSAEREFUSED: number; + } + namespace dlopen { + const RTLD_LAZY: number; + const RTLD_NOW: number; + const RTLD_GLOBAL: number; + const RTLD_LOCAL: number; + const RTLD_DEEPBIND: number; + } + namespace priority { + const PRIORITY_LOW: number; + const PRIORITY_BELOW_NORMAL: number; + const PRIORITY_NORMAL: number; + const PRIORITY_ABOVE_NORMAL: number; + const PRIORITY_HIGH: number; + const PRIORITY_HIGHEST: number; + } + } + const devNull: string; + /** + * The operating system-specific end-of-line marker. + * * `\n` on POSIX + * * `\r\n` on Windows + */ + const EOL: string; + /** + * Returns the operating system CPU architecture for which the Node.js binary was + * compiled. Possible values are `'arm'`, `'arm64'`, `'ia32'`, `'loong64'`, `'mips'`, `'mipsel'`, `'ppc'`, `'ppc64'`, `'riscv64'`, `'s390'`, `'s390x'`, + * and `'x64'`. + * + * The return value is equivalent to [process.arch](https://nodejs.org/docs/latest-v22.x/api/process.html#processarch). + * @since v0.5.0 + */ + function arch(): string; + /** + * Returns a string identifying the kernel version. + * + * On POSIX systems, the operating system release is determined by calling [`uname(3)`](https://linux.die.net/man/3/uname). On Windows, `RtlGetVersion()` is used, and if it is not + * available, `GetVersionExW()` will be used. See [https://en.wikipedia.org/wiki/Uname#Examples](https://en.wikipedia.org/wiki/Uname#Examples) for more information. + * @since v13.11.0, v12.17.0 + */ + function version(): string; + /** + * Returns a string identifying the operating system platform for which + * the Node.js binary was compiled. The value is set at compile time. + * Possible values are `'aix'`, `'darwin'`, `'freebsd'`, `'linux'`, `'openbsd'`, `'sunos'`, and `'win32'`. + * + * The return value is equivalent to `process.platform`. + * + * The value `'android'` may also be returned if Node.js is built on the Android + * operating system. [Android support is experimental](https://github.com/nodejs/node/blob/HEAD/BUILDING.md#androidandroid-based-devices-eg-firefox-os). + * @since v0.5.0 + */ + function platform(): NodeJS.Platform; + /** + * Returns the machine type as a string, such as `arm`, `arm64`, `aarch64`, `mips`, `mips64`, `ppc64`, `ppc64le`, `s390`, `s390x`, `i386`, `i686`, `x86_64`. + * + * On POSIX systems, the machine type is determined by calling [`uname(3)`](https://linux.die.net/man/3/uname). On Windows, `RtlGetVersion()` is used, and if it is not + * available, `GetVersionExW()` will be used. See [https://en.wikipedia.org/wiki/Uname#Examples](https://en.wikipedia.org/wiki/Uname#Examples) for more information. + * @since v18.9.0, v16.18.0 + */ + function machine(): string; + /** + * Returns the operating system's default directory for temporary files as a + * string. + * @since v0.9.9 + */ + function tmpdir(): string; + /** + * Returns a string identifying the endianness of the CPU for which the Node.js + * binary was compiled. + * + * Possible values are `'BE'` for big endian and `'LE'` for little endian. + * @since v0.9.4 + */ + function endianness(): "BE" | "LE"; + /** + * Returns the scheduling priority for the process specified by `pid`. If `pid` is + * not provided or is `0`, the priority of the current process is returned. + * @since v10.10.0 + * @param [pid=0] The process ID to retrieve scheduling priority for. + */ + function getPriority(pid?: number): number; + /** + * Attempts to set the scheduling priority for the process specified by `pid`. If `pid` is not provided or is `0`, the process ID of the current process is used. + * + * The `priority` input must be an integer between `-20` (high priority) and `19` (low priority). Due to differences between Unix priority levels and Windows + * priority classes, `priority` is mapped to one of six priority constants in `os.constants.priority`. When retrieving a process priority level, this range + * mapping may cause the return value to be slightly different on Windows. To avoid + * confusion, set `priority` to one of the priority constants. + * + * On Windows, setting priority to `PRIORITY_HIGHEST` requires elevated user + * privileges. Otherwise the set priority will be silently reduced to `PRIORITY_HIGH`. + * @since v10.10.0 + * @param [pid=0] The process ID to set scheduling priority for. + * @param priority The scheduling priority to assign to the process. + */ + function setPriority(priority: number): void; + function setPriority(pid: number, priority: number): void; +} +declare module "node:os" { + export * from "os"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/package.json new file mode 100644 index 00000000..b25bf268 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/package.json @@ -0,0 +1,145 @@ +{ + "name": "@types/node", + "version": "22.19.15", + "description": "TypeScript definitions for node", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node", + "license": "MIT", + "contributors": [ + { + "name": "Microsoft TypeScript", + "githubUsername": "Microsoft", + "url": "https://github.com/Microsoft" + }, + { + "name": "Alberto Schiabel", + "githubUsername": "jkomyno", + "url": "https://github.com/jkomyno" + }, + { + "name": "Andrew Makarov", + "githubUsername": "r3nya", + "url": "https://github.com/r3nya" + }, + { + "name": "Benjamin Toueg", + "githubUsername": "btoueg", + "url": "https://github.com/btoueg" + }, + { + "name": "David Junger", + "githubUsername": "touffy", + "url": "https://github.com/touffy" + }, + { + "name": "Mohsen Azimi", + "githubUsername": "mohsen1", + "url": "https://github.com/mohsen1" + }, + { + "name": "Nikita Galkin", + "githubUsername": "galkin", + "url": "https://github.com/galkin" + }, + { + "name": "Sebastian Silbermann", + "githubUsername": "eps1lon", + "url": "https://github.com/eps1lon" + }, + { + "name": "Wilco Bakker", + "githubUsername": "WilcoBakker", + "url": "https://github.com/WilcoBakker" + }, + { + "name": "Marcin Kopacz", + "githubUsername": "chyzwar", + "url": "https://github.com/chyzwar" + }, + { + "name": "Trivikram Kamat", + "githubUsername": "trivikr", + "url": "https://github.com/trivikr" + }, + { + "name": "Junxiao Shi", + "githubUsername": "yoursunny", + "url": "https://github.com/yoursunny" + }, + { + "name": "Ilia Baryshnikov", + "githubUsername": "qwelias", + "url": "https://github.com/qwelias" + }, + { + "name": "ExE Boss", + "githubUsername": "ExE-Boss", + "url": "https://github.com/ExE-Boss" + }, + { + "name": "Piotr Błażejewicz", + "githubUsername": "peterblazejewicz", + "url": "https://github.com/peterblazejewicz" + }, + { + "name": "Anna Henningsen", + "githubUsername": "addaleax", + "url": "https://github.com/addaleax" + }, + { + "name": "Victor Perin", + "githubUsername": "victorperin", + "url": "https://github.com/victorperin" + }, + { + "name": "NodeJS Contributors", + "githubUsername": "NodeJS", + "url": "https://github.com/NodeJS" + }, + { + "name": "Linus Unnebäck", + "githubUsername": "LinusU", + "url": "https://github.com/LinusU" + }, + { + "name": "wafuwafu13", + "githubUsername": "wafuwafu13", + "url": "https://github.com/wafuwafu13" + }, + { + "name": "Matteo Collina", + "githubUsername": "mcollina", + "url": "https://github.com/mcollina" + }, + { + "name": "Dmitry Semigradsky", + "githubUsername": "Semigradsky", + "url": "https://github.com/Semigradsky" + }, + { + "name": "René", + "githubUsername": "Renegade334", + "url": "https://github.com/Renegade334" + } + ], + "main": "", + "types": "index.d.ts", + "typesVersions": { + "<=5.6": { + "*": [ + "ts5.6/*" + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/node" + }, + "scripts": {}, + "dependencies": { + "undici-types": "~6.21.0" + }, + "peerDependencies": {}, + "typesPublisherContentHash": "e4f9ffdeab50c69a72c98fc00b007f9def62394c519695bf5f543d4d49d0a1c3", + "typeScriptVersion": "5.2" +} \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/path.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/path.d.ts new file mode 100644 index 00000000..b83d8f50 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/path.d.ts @@ -0,0 +1,200 @@ +declare module "path/posix" { + import path = require("path"); + export = path; +} +declare module "path/win32" { + import path = require("path"); + export = path; +} +/** + * The `node:path` module provides utilities for working with file and directory + * paths. It can be accessed using: + * + * ```js + * import path from 'node:path'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/path.js) + */ +declare module "path" { + namespace path { + /** + * A parsed path object generated by path.parse() or consumed by path.format(). + */ + interface ParsedPath { + /** + * The root of the path such as '/' or 'c:\' + */ + root: string; + /** + * The full directory path such as '/home/user/dir' or 'c:\path\dir' + */ + dir: string; + /** + * The file name including extension (if any) such as 'index.html' + */ + base: string; + /** + * The file extension (if any) such as '.html' + */ + ext: string; + /** + * The file name without extension (if any) such as 'index' + */ + name: string; + } + interface FormatInputPathObject { + /** + * The root of the path such as '/' or 'c:\' + */ + root?: string | undefined; + /** + * The full directory path such as '/home/user/dir' or 'c:\path\dir' + */ + dir?: string | undefined; + /** + * The file name including extension (if any) such as 'index.html' + */ + base?: string | undefined; + /** + * The file extension (if any) such as '.html' + */ + ext?: string | undefined; + /** + * The file name without extension (if any) such as 'index' + */ + name?: string | undefined; + } + interface PlatformPath { + /** + * Normalize a string path, reducing '..' and '.' parts. + * When multiple slashes are found, they're replaced by a single one; when the path contains a trailing slash, it is preserved. On Windows backslashes are used. If the path is a zero-length string, '.' is returned, representing the current working directory. + * + * @param path string path to normalize. + * @throws {TypeError} if `path` is not a string. + */ + normalize(path: string): string; + /** + * Join all arguments together and normalize the resulting path. + * + * @param paths paths to join. + * @throws {TypeError} if any of the path segments is not a string. + */ + join(...paths: string[]): string; + /** + * The right-most parameter is considered {to}. Other parameters are considered an array of {from}. + * + * Starting from leftmost {from} parameter, resolves {to} to an absolute path. + * + * If {to} isn't already absolute, {from} arguments are prepended in right to left order, + * until an absolute path is found. If after using all {from} paths still no absolute path is found, + * the current working directory is used as well. The resulting path is normalized, + * and trailing slashes are removed unless the path gets resolved to the root directory. + * + * @param paths A sequence of paths or path segments. + * @throws {TypeError} if any of the arguments is not a string. + */ + resolve(...paths: string[]): string; + /** + * The `path.matchesGlob()` method determines if `path` matches the `pattern`. + * @param path The path to glob-match against. + * @param pattern The glob to check the path against. + * @returns Whether or not the `path` matched the `pattern`. + * @throws {TypeError} if `path` or `pattern` are not strings. + * @since v22.5.0 + */ + matchesGlob(path: string, pattern: string): boolean; + /** + * Determines whether {path} is an absolute path. An absolute path will always resolve to the same location, regardless of the working directory. + * + * If the given {path} is a zero-length string, `false` will be returned. + * + * @param path path to test. + * @throws {TypeError} if `path` is not a string. + */ + isAbsolute(path: string): boolean; + /** + * Solve the relative path from {from} to {to} based on the current working directory. + * At times we have two absolute paths, and we need to derive the relative path from one to the other. This is actually the reverse transform of path.resolve. + * + * @throws {TypeError} if either `from` or `to` is not a string. + */ + relative(from: string, to: string): string; + /** + * Return the directory name of a path. Similar to the Unix dirname command. + * + * @param path the path to evaluate. + * @throws {TypeError} if `path` is not a string. + */ + dirname(path: string): string; + /** + * Return the last portion of a path. Similar to the Unix basename command. + * Often used to extract the file name from a fully qualified path. + * + * @param path the path to evaluate. + * @param suffix optionally, an extension to remove from the result. + * @throws {TypeError} if `path` is not a string or if `ext` is given and is not a string. + */ + basename(path: string, suffix?: string): string; + /** + * Return the extension of the path, from the last '.' to end of string in the last portion of the path. + * If there is no '.' in the last portion of the path or the first character of it is '.', then it returns an empty string. + * + * @param path the path to evaluate. + * @throws {TypeError} if `path` is not a string. + */ + extname(path: string): string; + /** + * The platform-specific file separator. '\\' or '/'. + */ + readonly sep: "\\" | "/"; + /** + * The platform-specific file delimiter. ';' or ':'. + */ + readonly delimiter: ";" | ":"; + /** + * Returns an object from a path string - the opposite of format(). + * + * @param path path to evaluate. + * @throws {TypeError} if `path` is not a string. + */ + parse(path: string): ParsedPath; + /** + * Returns a path string from an object - the opposite of parse(). + * + * @param pathObject path to evaluate. + */ + format(pathObject: FormatInputPathObject): string; + /** + * On Windows systems only, returns an equivalent namespace-prefixed path for the given path. + * If path is not a string, path will be returned without modifications. + * This method is meaningful only on Windows system. + * On POSIX systems, the method is non-operational and always returns path without modifications. + */ + toNamespacedPath(path: string): string; + /** + * Posix specific pathing. + * Same as parent object on posix. + */ + readonly posix: PlatformPath; + /** + * Windows specific pathing. + * Same as parent object on windows + */ + readonly win32: PlatformPath; + } + } + const path: path.PlatformPath; + export = path; +} +declare module "node:path" { + import path = require("path"); + export = path; +} +declare module "node:path/posix" { + import path = require("path/posix"); + export = path; +} +declare module "node:path/win32" { + import path = require("path/win32"); + export = path; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/perf_hooks.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/perf_hooks.d.ts new file mode 100644 index 00000000..ad0785de --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/perf_hooks.d.ts @@ -0,0 +1,968 @@ +/** + * This module provides an implementation of a subset of the W3C [Web Performance APIs](https://w3c.github.io/perf-timing-primer/) as well as additional APIs for + * Node.js-specific performance measurements. + * + * Node.js supports the following [Web Performance APIs](https://w3c.github.io/perf-timing-primer/): + * + * * [High Resolution Time](https://www.w3.org/TR/hr-time-2) + * * [Performance Timeline](https://w3c.github.io/performance-timeline/) + * * [User Timing](https://www.w3.org/TR/user-timing/) + * * [Resource Timing](https://www.w3.org/TR/resource-timing-2/) + * + * ```js + * import { PerformanceObserver, performance } from 'node:perf_hooks'; + * + * const obs = new PerformanceObserver((items) => { + * console.log(items.getEntries()[0].duration); + * performance.clearMarks(); + * }); + * obs.observe({ type: 'measure' }); + * performance.measure('Start to Now'); + * + * performance.mark('A'); + * doSomeLongRunningProcess(() => { + * performance.measure('A to Now', 'A'); + * + * performance.mark('B'); + * performance.measure('A to B', 'A', 'B'); + * }); + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/perf_hooks.js) + */ +declare module "perf_hooks" { + import { AsyncResource } from "node:async_hooks"; + type EntryType = + | "dns" // Node.js only + | "function" // Node.js only + | "gc" // Node.js only + | "http2" // Node.js only + | "http" // Node.js only + | "mark" // available on the Web + | "measure" // available on the Web + | "net" // Node.js only + | "node" // Node.js only + | "resource"; // available on the Web + interface NodeGCPerformanceDetail { + /** + * When `performanceEntry.entryType` is equal to 'gc', the `performance.kind` property identifies + * the type of garbage collection operation that occurred. + * See perf_hooks.constants for valid values. + */ + readonly kind: number; + /** + * When `performanceEntry.entryType` is equal to 'gc', the `performance.flags` + * property contains additional information about garbage collection operation. + * See perf_hooks.constants for valid values. + */ + readonly flags: number; + } + /** + * The constructor of this class is not exposed to users directly. + * @since v8.5.0 + */ + class PerformanceEntry { + protected constructor(); + /** + * The total number of milliseconds elapsed for this entry. This value will not + * be meaningful for all Performance Entry types. + * @since v8.5.0 + */ + readonly duration: number; + /** + * The name of the performance entry. + * @since v8.5.0 + */ + readonly name: string; + /** + * The high resolution millisecond timestamp marking the starting time of the + * Performance Entry. + * @since v8.5.0 + */ + readonly startTime: number; + /** + * The type of the performance entry. It may be one of: + * + * * `'node'` (Node.js only) + * * `'mark'` (available on the Web) + * * `'measure'` (available on the Web) + * * `'gc'` (Node.js only) + * * `'function'` (Node.js only) + * * `'http2'` (Node.js only) + * * `'http'` (Node.js only) + * @since v8.5.0 + */ + readonly entryType: EntryType; + toJSON(): any; + } + /** + * Exposes marks created via the `Performance.mark()` method. + * @since v18.2.0, v16.17.0 + */ + class PerformanceMark extends PerformanceEntry { + readonly detail: any; + readonly duration: 0; + readonly entryType: "mark"; + } + /** + * Exposes measures created via the `Performance.measure()` method. + * + * The constructor of this class is not exposed to users directly. + * @since v18.2.0, v16.17.0 + */ + class PerformanceMeasure extends PerformanceEntry { + readonly detail: any; + readonly entryType: "measure"; + } + interface UVMetrics { + /** + * Number of event loop iterations. + */ + readonly loopCount: number; + /** + * Number of events that have been processed by the event handler. + */ + readonly events: number; + /** + * Number of events that were waiting to be processed when the event provider was called. + */ + readonly eventsWaiting: number; + } + // TODO: PerformanceNodeEntry is missing + /** + * _This property is an extension by Node.js. It is not available in Web browsers._ + * + * Provides timing details for Node.js itself. The constructor of this class + * is not exposed to users. + * @since v8.5.0 + */ + class PerformanceNodeTiming extends PerformanceEntry { + readonly entryType: "node"; + /** + * The high resolution millisecond timestamp at which the Node.js process + * completed bootstrapping. If bootstrapping has not yet finished, the property + * has the value of -1. + * @since v8.5.0 + */ + readonly bootstrapComplete: number; + /** + * The high resolution millisecond timestamp at which the Node.js environment was + * initialized. + * @since v8.5.0 + */ + readonly environment: number; + /** + * The high resolution millisecond timestamp of the amount of time the event loop + * has been idle within the event loop's event provider (e.g. `epoll_wait`). This + * does not take CPU usage into consideration. If the event loop has not yet + * started (e.g., in the first tick of the main script), the property has the + * value of 0. + * @since v14.10.0, v12.19.0 + */ + readonly idleTime: number; + /** + * The high resolution millisecond timestamp at which the Node.js event loop + * exited. If the event loop has not yet exited, the property has the value of -1\. + * It can only have a value of not -1 in a handler of the `'exit'` event. + * @since v8.5.0 + */ + readonly loopExit: number; + /** + * The high resolution millisecond timestamp at which the Node.js event loop + * started. If the event loop has not yet started (e.g., in the first tick of the + * main script), the property has the value of -1. + * @since v8.5.0 + */ + readonly loopStart: number; + /** + * The high resolution millisecond timestamp at which the Node.js process was initialized. + * @since v8.5.0 + */ + readonly nodeStart: number; + /** + * This is a wrapper to the `uv_metrics_info` function. + * It returns the current set of event loop metrics. + * + * It is recommended to use this property inside a function whose execution was + * scheduled using `setImmediate` to avoid collecting metrics before finishing all + * operations scheduled during the current loop iteration. + * @since v22.8.0, v20.18.0 + */ + readonly uvMetricsInfo: UVMetrics; + /** + * The high resolution millisecond timestamp at which the V8 platform was + * initialized. + * @since v8.5.0 + */ + readonly v8Start: number; + } + interface EventLoopUtilization { + idle: number; + active: number; + utilization: number; + } + /** + * @param utilization1 The result of a previous call to `eventLoopUtilization()`. + * @param utilization2 The result of a previous call to `eventLoopUtilization()` prior to `utilization1`. + */ + type EventLoopUtilityFunction = ( + utilization1?: EventLoopUtilization, + utilization2?: EventLoopUtilization, + ) => EventLoopUtilization; + interface MarkOptions { + /** + * Additional optional detail to include with the mark. + */ + detail?: unknown | undefined; + /** + * An optional timestamp to be used as the mark time. + * @default `performance.now()` + */ + startTime?: number | undefined; + } + interface MeasureOptions { + /** + * Additional optional detail to include with the mark. + */ + detail?: unknown; + /** + * Duration between start and end times. + */ + duration?: number | undefined; + /** + * Timestamp to be used as the end time, or a string identifying a previously recorded mark. + */ + end?: number | string | undefined; + /** + * Timestamp to be used as the start time, or a string identifying a previously recorded mark. + */ + start?: number | string | undefined; + } + interface TimerifyOptions { + /** + * A histogram object created using `perf_hooks.createHistogram()` that will record runtime + * durations in nanoseconds. + */ + histogram?: RecordableHistogram | undefined; + } + interface Performance { + /** + * If `name` is not provided, removes all `PerformanceMark` objects from the Performance Timeline. + * If `name` is provided, removes only the named mark. + * @since v8.5.0 + */ + clearMarks(name?: string): void; + /** + * If `name` is not provided, removes all `PerformanceMeasure` objects from the Performance Timeline. + * If `name` is provided, removes only the named measure. + * @since v16.7.0 + */ + clearMeasures(name?: string): void; + /** + * If `name` is not provided, removes all `PerformanceResourceTiming` objects from the Resource Timeline. + * If `name` is provided, removes only the named resource. + * @since v18.2.0, v16.17.0 + */ + clearResourceTimings(name?: string): void; + /** + * eventLoopUtilization is similar to CPU utilization except that it is calculated using high precision wall-clock time. + * It represents the percentage of time the event loop has spent outside the event loop's event provider (e.g. epoll_wait). + * No other CPU idle time is taken into consideration. + */ + eventLoopUtilization: EventLoopUtilityFunction; + /** + * Returns a list of `PerformanceEntry` objects in chronological order with respect to `performanceEntry.startTime`. + * If you are only interested in performance entries of certain types or that have certain names, see + * `performance.getEntriesByType()` and `performance.getEntriesByName()`. + * @since v16.7.0 + */ + getEntries(): PerformanceEntry[]; + /** + * Returns a list of `PerformanceEntry` objects in chronological order with respect to `performanceEntry.startTime` + * whose `performanceEntry.name` is equal to `name`, and optionally, whose `performanceEntry.entryType` is equal to `type`. + * @param name + * @param type + * @since v16.7.0 + */ + getEntriesByName(name: string, type?: EntryType): PerformanceEntry[]; + /** + * Returns a list of `PerformanceEntry` objects in chronological order with respect to `performanceEntry.startTime` + * whose `performanceEntry.entryType` is equal to `type`. + * @param type + * @since v16.7.0 + */ + getEntriesByType(type: EntryType): PerformanceEntry[]; + /** + * Creates a new `PerformanceMark` entry in the Performance Timeline. + * A `PerformanceMark` is a subclass of `PerformanceEntry` whose `performanceEntry.entryType` is always `'mark'`, + * and whose `performanceEntry.duration` is always `0`. + * Performance marks are used to mark specific significant moments in the Performance Timeline. + * + * The created `PerformanceMark` entry is put in the global Performance Timeline and can be queried with + * `performance.getEntries`, `performance.getEntriesByName`, and `performance.getEntriesByType`. When the observation is + * performed, the entries should be cleared from the global Performance Timeline manually with `performance.clearMarks`. + * @param name + */ + mark(name: string, options?: MarkOptions): PerformanceMark; + /** + * Creates a new `PerformanceResourceTiming` entry in the Resource Timeline. + * A `PerformanceResourceTiming` is a subclass of `PerformanceEntry` whose `performanceEntry.entryType` is always `'resource'`. + * Performance resources are used to mark moments in the Resource Timeline. + * @param timingInfo [Fetch Timing Info](https://fetch.spec.whatwg.org/#fetch-timing-info) + * @param requestedUrl The resource url + * @param initiatorType The initiator name, e.g: 'fetch' + * @param global + * @param cacheMode The cache mode must be an empty string ('') or 'local' + * @param bodyInfo [Fetch Response Body Info](https://fetch.spec.whatwg.org/#response-body-info) + * @param responseStatus The response's status code + * @param deliveryType The delivery type. Default: ''. + * @since v18.2.0, v16.17.0 + */ + markResourceTiming( + timingInfo: object, + requestedUrl: string, + initiatorType: string, + global: object, + cacheMode: "" | "local", + bodyInfo: object, + responseStatus: number, + deliveryType?: string, + ): PerformanceResourceTiming; + /** + * Creates a new PerformanceMeasure entry in the Performance Timeline. + * A PerformanceMeasure is a subclass of PerformanceEntry whose performanceEntry.entryType is always 'measure', + * and whose performanceEntry.duration measures the number of milliseconds elapsed since startMark and endMark. + * + * The startMark argument may identify any existing PerformanceMark in the the Performance Timeline, or may identify + * any of the timestamp properties provided by the PerformanceNodeTiming class. If the named startMark does not exist, + * then startMark is set to timeOrigin by default. + * + * The endMark argument must identify any existing PerformanceMark in the the Performance Timeline or any of the timestamp + * properties provided by the PerformanceNodeTiming class. If the named endMark does not exist, an error will be thrown. + * @param name + * @param startMark + * @param endMark + * @return The PerformanceMeasure entry that was created + */ + measure(name: string, startMark?: string, endMark?: string): PerformanceMeasure; + measure(name: string, options: MeasureOptions): PerformanceMeasure; + /** + * _This property is an extension by Node.js. It is not available in Web browsers._ + * + * An instance of the `PerformanceNodeTiming` class that provides performance metrics for specific Node.js operational milestones. + * @since v8.5.0 + */ + readonly nodeTiming: PerformanceNodeTiming; + /** + * Returns the current high resolution millisecond timestamp, where 0 represents the start of the current `node` process. + * @since v8.5.0 + */ + now(): number; + /** + * Sets the global performance resource timing buffer size to the specified number of "resource" type performance entry objects. + * + * By default the max buffer size is set to 250. + * @since v18.8.0 + */ + setResourceTimingBufferSize(maxSize: number): void; + /** + * The [`timeOrigin`](https://w3c.github.io/hr-time/#dom-performance-timeorigin) specifies the high resolution millisecond timestamp + * at which the current `node` process began, measured in Unix time. + * @since v8.5.0 + */ + readonly timeOrigin: number; + /** + * _This property is an extension by Node.js. It is not available in Web browsers._ + * + * Wraps a function within a new function that measures the running time of the wrapped function. + * A `PerformanceObserver` must be subscribed to the `'function'` event type in order for the timing details to be accessed. + * + * ```js + * import { + * performance, + * PerformanceObserver, + * } from 'node:perf_hooks'; + * + * function someFunction() { + * console.log('hello world'); + * } + * + * const wrapped = performance.timerify(someFunction); + * + * const obs = new PerformanceObserver((list) => { + * console.log(list.getEntries()[0].duration); + * + * performance.clearMarks(); + * performance.clearMeasures(); + * obs.disconnect(); + * }); + * obs.observe({ entryTypes: ['function'] }); + * + * // A performance timeline entry will be created + * wrapped(); + * ``` + * + * If the wrapped function returns a promise, a finally handler will be attached to the promise and the duration will be reported + * once the finally handler is invoked. + * @param fn + */ + timerify any>(fn: T, options?: TimerifyOptions): T; + /** + * An object which is JSON representation of the performance object. It is similar to + * [`window.performance.toJSON`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/toJSON) in browsers. + * @since v16.1.0 + */ + toJSON(): any; + } + class PerformanceObserverEntryList { + /** + * Returns a list of `PerformanceEntry` objects in chronological order + * with respect to `performanceEntry.startTime`. + * + * ```js + * import { + * performance, + * PerformanceObserver, + * } from 'node:perf_hooks'; + * + * const obs = new PerformanceObserver((perfObserverList, observer) => { + * console.log(perfObserverList.getEntries()); + * + * * [ + * * PerformanceEntry { + * * name: 'test', + * * entryType: 'mark', + * * startTime: 81.465639, + * * duration: 0, + * * detail: null + * * }, + * * PerformanceEntry { + * * name: 'meow', + * * entryType: 'mark', + * * startTime: 81.860064, + * * duration: 0, + * * detail: null + * * } + * * ] + * + * performance.clearMarks(); + * performance.clearMeasures(); + * observer.disconnect(); + * }); + * obs.observe({ type: 'mark' }); + * + * performance.mark('test'); + * performance.mark('meow'); + * ``` + * @since v8.5.0 + */ + getEntries(): PerformanceEntry[]; + /** + * Returns a list of `PerformanceEntry` objects in chronological order + * with respect to `performanceEntry.startTime` whose `performanceEntry.name` is + * equal to `name`, and optionally, whose `performanceEntry.entryType` is equal to`type`. + * + * ```js + * import { + * performance, + * PerformanceObserver, + * } from 'node:perf_hooks'; + * + * const obs = new PerformanceObserver((perfObserverList, observer) => { + * console.log(perfObserverList.getEntriesByName('meow')); + * + * * [ + * * PerformanceEntry { + * * name: 'meow', + * * entryType: 'mark', + * * startTime: 98.545991, + * * duration: 0, + * * detail: null + * * } + * * ] + * + * console.log(perfObserverList.getEntriesByName('nope')); // [] + * + * console.log(perfObserverList.getEntriesByName('test', 'mark')); + * + * * [ + * * PerformanceEntry { + * * name: 'test', + * * entryType: 'mark', + * * startTime: 63.518931, + * * duration: 0, + * * detail: null + * * } + * * ] + * + * console.log(perfObserverList.getEntriesByName('test', 'measure')); // [] + * + * performance.clearMarks(); + * performance.clearMeasures(); + * observer.disconnect(); + * }); + * obs.observe({ entryTypes: ['mark', 'measure'] }); + * + * performance.mark('test'); + * performance.mark('meow'); + * ``` + * @since v8.5.0 + */ + getEntriesByName(name: string, type?: EntryType): PerformanceEntry[]; + /** + * Returns a list of `PerformanceEntry` objects in chronological order + * with respect to `performanceEntry.startTime` whose `performanceEntry.entryType` is equal to `type`. + * + * ```js + * import { + * performance, + * PerformanceObserver, + * } from 'node:perf_hooks'; + * + * const obs = new PerformanceObserver((perfObserverList, observer) => { + * console.log(perfObserverList.getEntriesByType('mark')); + * + * * [ + * * PerformanceEntry { + * * name: 'test', + * * entryType: 'mark', + * * startTime: 55.897834, + * * duration: 0, + * * detail: null + * * }, + * * PerformanceEntry { + * * name: 'meow', + * * entryType: 'mark', + * * startTime: 56.350146, + * * duration: 0, + * * detail: null + * * } + * * ] + * + * performance.clearMarks(); + * performance.clearMeasures(); + * observer.disconnect(); + * }); + * obs.observe({ type: 'mark' }); + * + * performance.mark('test'); + * performance.mark('meow'); + * ``` + * @since v8.5.0 + */ + getEntriesByType(type: EntryType): PerformanceEntry[]; + } + type PerformanceObserverCallback = (list: PerformanceObserverEntryList, observer: PerformanceObserver) => void; + /** + * @since v8.5.0 + */ + class PerformanceObserver extends AsyncResource { + constructor(callback: PerformanceObserverCallback); + /** + * Disconnects the `PerformanceObserver` instance from all notifications. + * @since v8.5.0 + */ + disconnect(): void; + /** + * Subscribes the `PerformanceObserver` instance to notifications of new `PerformanceEntry` instances identified either by `options.entryTypes` or `options.type`: + * + * ```js + * import { + * performance, + * PerformanceObserver, + * } from 'node:perf_hooks'; + * + * const obs = new PerformanceObserver((list, observer) => { + * // Called once asynchronously. `list` contains three items. + * }); + * obs.observe({ type: 'mark' }); + * + * for (let n = 0; n < 3; n++) + * performance.mark(`test${n}`); + * ``` + * @since v8.5.0 + */ + observe( + options: + | { + entryTypes: readonly EntryType[]; + buffered?: boolean | undefined; + } + | { + type: EntryType; + buffered?: boolean | undefined; + }, + ): void; + /** + * @since v16.0.0 + * @returns Current list of entries stored in the performance observer, emptying it out. + */ + takeRecords(): PerformanceEntry[]; + } + /** + * Provides detailed network timing data regarding the loading of an application's resources. + * + * The constructor of this class is not exposed to users directly. + * @since v18.2.0, v16.17.0 + */ + class PerformanceResourceTiming extends PerformanceEntry { + readonly entryType: "resource"; + protected constructor(); + /** + * The high resolution millisecond timestamp at immediately before dispatching the `fetch` + * request. If the resource is not intercepted by a worker the property will always return 0. + * @since v18.2.0, v16.17.0 + */ + readonly workerStart: number; + /** + * The high resolution millisecond timestamp that represents the start time of the fetch which + * initiates the redirect. + * @since v18.2.0, v16.17.0 + */ + readonly redirectStart: number; + /** + * The high resolution millisecond timestamp that will be created immediately after receiving + * the last byte of the response of the last redirect. + * @since v18.2.0, v16.17.0 + */ + readonly redirectEnd: number; + /** + * The high resolution millisecond timestamp immediately before the Node.js starts to fetch the resource. + * @since v18.2.0, v16.17.0 + */ + readonly fetchStart: number; + /** + * The high resolution millisecond timestamp immediately before the Node.js starts the domain name lookup + * for the resource. + * @since v18.2.0, v16.17.0 + */ + readonly domainLookupStart: number; + /** + * The high resolution millisecond timestamp representing the time immediately after the Node.js finished + * the domain name lookup for the resource. + * @since v18.2.0, v16.17.0 + */ + readonly domainLookupEnd: number; + /** + * The high resolution millisecond timestamp representing the time immediately before Node.js starts to + * establish the connection to the server to retrieve the resource. + * @since v18.2.0, v16.17.0 + */ + readonly connectStart: number; + /** + * The high resolution millisecond timestamp representing the time immediately after Node.js finishes + * establishing the connection to the server to retrieve the resource. + * @since v18.2.0, v16.17.0 + */ + readonly connectEnd: number; + /** + * The high resolution millisecond timestamp representing the time immediately before Node.js starts the + * handshake process to secure the current connection. + * @since v18.2.0, v16.17.0 + */ + readonly secureConnectionStart: number; + /** + * The high resolution millisecond timestamp representing the time immediately before Node.js receives the + * first byte of the response from the server. + * @since v18.2.0, v16.17.0 + */ + readonly requestStart: number; + /** + * The high resolution millisecond timestamp representing the time immediately after Node.js receives the + * last byte of the resource or immediately before the transport connection is closed, whichever comes first. + * @since v18.2.0, v16.17.0 + */ + readonly responseEnd: number; + /** + * A number representing the size (in octets) of the fetched resource. The size includes the response header + * fields plus the response payload body. + * @since v18.2.0, v16.17.0 + */ + readonly transferSize: number; + /** + * A number representing the size (in octets) received from the fetch (HTTP or cache), of the payload body, before + * removing any applied content-codings. + * @since v18.2.0, v16.17.0 + */ + readonly encodedBodySize: number; + /** + * A number representing the size (in octets) received from the fetch (HTTP or cache), of the message body, after + * removing any applied content-codings. + * @since v18.2.0, v16.17.0 + */ + readonly decodedBodySize: number; + /** + * Returns a `object` that is the JSON representation of the `PerformanceResourceTiming` object + * @since v18.2.0, v16.17.0 + */ + toJSON(): any; + } + namespace constants { + const NODE_PERFORMANCE_GC_MAJOR: number; + const NODE_PERFORMANCE_GC_MINOR: number; + const NODE_PERFORMANCE_GC_INCREMENTAL: number; + const NODE_PERFORMANCE_GC_WEAKCB: number; + const NODE_PERFORMANCE_GC_FLAGS_NO: number; + const NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED: number; + const NODE_PERFORMANCE_GC_FLAGS_FORCED: number; + const NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING: number; + const NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE: number; + const NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY: number; + const NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE: number; + } + const performance: Performance; + interface EventLoopMonitorOptions { + /** + * The sampling rate in milliseconds. + * Must be greater than zero. + * @default 10 + */ + resolution?: number | undefined; + } + interface Histogram { + /** + * The number of samples recorded by the histogram. + * @since v17.4.0, v16.14.0 + */ + readonly count: number; + /** + * The number of samples recorded by the histogram. + * v17.4.0, v16.14.0 + */ + readonly countBigInt: bigint; + /** + * The number of times the event loop delay exceeded the maximum 1 hour event + * loop delay threshold. + * @since v11.10.0 + */ + readonly exceeds: number; + /** + * The number of times the event loop delay exceeded the maximum 1 hour event loop delay threshold. + * @since v17.4.0, v16.14.0 + */ + readonly exceedsBigInt: bigint; + /** + * The maximum recorded event loop delay. + * @since v11.10.0 + */ + readonly max: number; + /** + * The maximum recorded event loop delay. + * v17.4.0, v16.14.0 + */ + readonly maxBigInt: number; + /** + * The mean of the recorded event loop delays. + * @since v11.10.0 + */ + readonly mean: number; + /** + * The minimum recorded event loop delay. + * @since v11.10.0 + */ + readonly min: number; + /** + * The minimum recorded event loop delay. + * v17.4.0, v16.14.0 + */ + readonly minBigInt: bigint; + /** + * Returns the value at the given percentile. + * @since v11.10.0 + * @param percentile A percentile value in the range (0, 100]. + */ + percentile(percentile: number): number; + /** + * Returns the value at the given percentile. + * @since v17.4.0, v16.14.0 + * @param percentile A percentile value in the range (0, 100]. + */ + percentileBigInt(percentile: number): bigint; + /** + * Returns a `Map` object detailing the accumulated percentile distribution. + * @since v11.10.0 + */ + readonly percentiles: Map; + /** + * Returns a `Map` object detailing the accumulated percentile distribution. + * @since v17.4.0, v16.14.0 + */ + readonly percentilesBigInt: Map; + /** + * Resets the collected histogram data. + * @since v11.10.0 + */ + reset(): void; + /** + * The standard deviation of the recorded event loop delays. + * @since v11.10.0 + */ + readonly stddev: number; + } + interface IntervalHistogram extends Histogram { + /** + * Enables the update interval timer. Returns `true` if the timer was + * started, `false` if it was already started. + * @since v11.10.0 + */ + enable(): boolean; + /** + * Disables the update interval timer. Returns `true` if the timer was + * stopped, `false` if it was already stopped. + * @since v11.10.0 + */ + disable(): boolean; + } + interface RecordableHistogram extends Histogram { + /** + * @since v15.9.0, v14.18.0 + * @param val The amount to record in the histogram. + */ + record(val: number | bigint): void; + /** + * Calculates the amount of time (in nanoseconds) that has passed since the + * previous call to `recordDelta()` and records that amount in the histogram. + * @since v15.9.0, v14.18.0 + */ + recordDelta(): void; + /** + * Adds the values from `other` to this histogram. + * @since v17.4.0, v16.14.0 + */ + add(other: RecordableHistogram): void; + } + /** + * _This property is an extension by Node.js. It is not available in Web browsers._ + * + * Creates an `IntervalHistogram` object that samples and reports the event loop + * delay over time. The delays will be reported in nanoseconds. + * + * Using a timer to detect approximate event loop delay works because the + * execution of timers is tied specifically to the lifecycle of the libuv + * event loop. That is, a delay in the loop will cause a delay in the execution + * of the timer, and those delays are specifically what this API is intended to + * detect. + * + * ```js + * import { monitorEventLoopDelay } from 'node:perf_hooks'; + * const h = monitorEventLoopDelay({ resolution: 20 }); + * h.enable(); + * // Do something. + * h.disable(); + * console.log(h.min); + * console.log(h.max); + * console.log(h.mean); + * console.log(h.stddev); + * console.log(h.percentiles); + * console.log(h.percentile(50)); + * console.log(h.percentile(99)); + * ``` + * @since v11.10.0 + */ + function monitorEventLoopDelay(options?: EventLoopMonitorOptions): IntervalHistogram; + interface CreateHistogramOptions { + /** + * The minimum recordable value. Must be an integer value greater than 0. + * @default 1 + */ + lowest?: number | bigint | undefined; + /** + * The maximum recordable value. Must be an integer value greater than min. + * @default Number.MAX_SAFE_INTEGER + */ + highest?: number | bigint | undefined; + /** + * The number of accuracy digits. Must be a number between 1 and 5. + * @default 3 + */ + figures?: number | undefined; + } + /** + * Returns a `RecordableHistogram`. + * @since v15.9.0, v14.18.0 + */ + function createHistogram(options?: CreateHistogramOptions): RecordableHistogram; + import { + performance as _performance, + PerformanceEntry as _PerformanceEntry, + PerformanceMark as _PerformanceMark, + PerformanceMeasure as _PerformanceMeasure, + PerformanceObserver as _PerformanceObserver, + PerformanceObserverEntryList as _PerformanceObserverEntryList, + PerformanceResourceTiming as _PerformanceResourceTiming, + } from "perf_hooks"; + global { + /** + * `PerformanceEntry` is a global reference for `import { PerformanceEntry } from 'node:perf_hooks'` + * @see https://nodejs.org/docs/latest-v22.x/api/globals.html#performanceentry + * @since v19.0.0 + */ + var PerformanceEntry: typeof globalThis extends { + onmessage: any; + PerformanceEntry: infer T; + } ? T + : typeof _PerformanceEntry; + /** + * `PerformanceMark` is a global reference for `import { PerformanceMark } from 'node:perf_hooks'` + * @see https://nodejs.org/docs/latest-v22.x/api/globals.html#performancemark + * @since v19.0.0 + */ + var PerformanceMark: typeof globalThis extends { + onmessage: any; + PerformanceMark: infer T; + } ? T + : typeof _PerformanceMark; + /** + * `PerformanceMeasure` is a global reference for `import { PerformanceMeasure } from 'node:perf_hooks'` + * @see https://nodejs.org/docs/latest-v22.x/api/globals.html#performancemeasure + * @since v19.0.0 + */ + var PerformanceMeasure: typeof globalThis extends { + onmessage: any; + PerformanceMeasure: infer T; + } ? T + : typeof _PerformanceMeasure; + /** + * `PerformanceObserver` is a global reference for `import { PerformanceObserver } from 'node:perf_hooks'` + * @see https://nodejs.org/docs/latest-v22.x/api/globals.html#performanceobserver + * @since v19.0.0 + */ + var PerformanceObserver: typeof globalThis extends { + onmessage: any; + PerformanceObserver: infer T; + } ? T + : typeof _PerformanceObserver; + /** + * `PerformanceObserverEntryList` is a global reference for `import { PerformanceObserverEntryList } from 'node:perf_hooks'` + * @see https://nodejs.org/docs/latest-v22.x/api/globals.html#performanceobserverentrylist + * @since v19.0.0 + */ + var PerformanceObserverEntryList: typeof globalThis extends { + onmessage: any; + PerformanceObserverEntryList: infer T; + } ? T + : typeof _PerformanceObserverEntryList; + /** + * `PerformanceResourceTiming` is a global reference for `import { PerformanceResourceTiming } from 'node:perf_hooks'` + * @see https://nodejs.org/docs/latest-v22.x/api/globals.html#performanceresourcetiming + * @since v19.0.0 + */ + var PerformanceResourceTiming: typeof globalThis extends { + onmessage: any; + PerformanceResourceTiming: infer T; + } ? T + : typeof _PerformanceResourceTiming; + /** + * `performance` is a global reference for `import { performance } from 'node:perf_hooks'` + * @see https://nodejs.org/docs/latest-v22.x/api/globals.html#performance + * @since v16.0.0 + */ + var performance: typeof globalThis extends { + onmessage: any; + performance: infer T; + } ? T + : typeof _performance; + } +} +declare module "node:perf_hooks" { + export * from "perf_hooks"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/process.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/process.d.ts new file mode 100644 index 00000000..10738398 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/process.d.ts @@ -0,0 +1,2084 @@ +declare module "process" { + import { Control, MessageOptions, SendHandle } from "node:child_process"; + import { PathLike } from "node:fs"; + import * as tty from "node:tty"; + import { Worker } from "node:worker_threads"; + + interface BuiltInModule { + "assert": typeof import("assert"); + "node:assert": typeof import("node:assert"); + "assert/strict": typeof import("assert/strict"); + "node:assert/strict": typeof import("node:assert/strict"); + "async_hooks": typeof import("async_hooks"); + "node:async_hooks": typeof import("node:async_hooks"); + "buffer": typeof import("buffer"); + "node:buffer": typeof import("node:buffer"); + "child_process": typeof import("child_process"); + "node:child_process": typeof import("node:child_process"); + "cluster": typeof import("cluster"); + "node:cluster": typeof import("node:cluster"); + "console": typeof import("console"); + "node:console": typeof import("node:console"); + "constants": typeof import("constants"); + "node:constants": typeof import("node:constants"); + "crypto": typeof import("crypto"); + "node:crypto": typeof import("node:crypto"); + "dgram": typeof import("dgram"); + "node:dgram": typeof import("node:dgram"); + "diagnostics_channel": typeof import("diagnostics_channel"); + "node:diagnostics_channel": typeof import("node:diagnostics_channel"); + "dns": typeof import("dns"); + "node:dns": typeof import("node:dns"); + "dns/promises": typeof import("dns/promises"); + "node:dns/promises": typeof import("node:dns/promises"); + "domain": typeof import("domain"); + "node:domain": typeof import("node:domain"); + "events": typeof import("events"); + "node:events": typeof import("node:events"); + "fs": typeof import("fs"); + "node:fs": typeof import("node:fs"); + "fs/promises": typeof import("fs/promises"); + "node:fs/promises": typeof import("node:fs/promises"); + "http": typeof import("http"); + "node:http": typeof import("node:http"); + "http2": typeof import("http2"); + "node:http2": typeof import("node:http2"); + "https": typeof import("https"); + "node:https": typeof import("node:https"); + "inspector": typeof import("inspector"); + "node:inspector": typeof import("node:inspector"); + "inspector/promises": typeof import("inspector/promises"); + "node:inspector/promises": typeof import("node:inspector/promises"); + "module": typeof import("module"); + "node:module": typeof import("node:module"); + "net": typeof import("net"); + "node:net": typeof import("node:net"); + "os": typeof import("os"); + "node:os": typeof import("node:os"); + "path": typeof import("path"); + "node:path": typeof import("node:path"); + "path/posix": typeof import("path/posix"); + "node:path/posix": typeof import("node:path/posix"); + "path/win32": typeof import("path/win32"); + "node:path/win32": typeof import("node:path/win32"); + "perf_hooks": typeof import("perf_hooks"); + "node:perf_hooks": typeof import("node:perf_hooks"); + "process": typeof import("process"); + "node:process": typeof import("node:process"); + "punycode": typeof import("punycode"); + "node:punycode": typeof import("node:punycode"); + "querystring": typeof import("querystring"); + "node:querystring": typeof import("node:querystring"); + "readline": typeof import("readline"); + "node:readline": typeof import("node:readline"); + "readline/promises": typeof import("readline/promises"); + "node:readline/promises": typeof import("node:readline/promises"); + "repl": typeof import("repl"); + "node:repl": typeof import("node:repl"); + "node:sea": typeof import("node:sea"); + "node:sqlite": typeof import("node:sqlite"); + "stream": typeof import("stream"); + "node:stream": typeof import("node:stream"); + "stream/consumers": typeof import("stream/consumers"); + "node:stream/consumers": typeof import("node:stream/consumers"); + "stream/promises": typeof import("stream/promises"); + "node:stream/promises": typeof import("node:stream/promises"); + "stream/web": typeof import("stream/web"); + "node:stream/web": typeof import("node:stream/web"); + "string_decoder": typeof import("string_decoder"); + "node:string_decoder": typeof import("node:string_decoder"); + "node:test": typeof import("node:test"); + "node:test/reporters": typeof import("node:test/reporters"); + "timers": typeof import("timers"); + "node:timers": typeof import("node:timers"); + "timers/promises": typeof import("timers/promises"); + "node:timers/promises": typeof import("node:timers/promises"); + "tls": typeof import("tls"); + "node:tls": typeof import("node:tls"); + "trace_events": typeof import("trace_events"); + "node:trace_events": typeof import("node:trace_events"); + "tty": typeof import("tty"); + "node:tty": typeof import("node:tty"); + "url": typeof import("url"); + "node:url": typeof import("node:url"); + "util": typeof import("util"); + "node:util": typeof import("node:util"); + "sys": typeof import("util"); + "node:sys": typeof import("node:util"); + "util/types": typeof import("util/types"); + "node:util/types": typeof import("node:util/types"); + "v8": typeof import("v8"); + "node:v8": typeof import("node:v8"); + "vm": typeof import("vm"); + "node:vm": typeof import("node:vm"); + "wasi": typeof import("wasi"); + "node:wasi": typeof import("node:wasi"); + "worker_threads": typeof import("worker_threads"); + "node:worker_threads": typeof import("node:worker_threads"); + "zlib": typeof import("zlib"); + "node:zlib": typeof import("node:zlib"); + } + global { + var process: NodeJS.Process; + namespace NodeJS { + // this namespace merge is here because these are specifically used + // as the type for process.stdin, process.stdout, and process.stderr. + // they can't live in tty.d.ts because we need to disambiguate the imported name. + interface ReadStream extends tty.ReadStream {} + interface WriteStream extends tty.WriteStream {} + interface MemoryUsageFn { + /** + * The `process.memoryUsage()` method iterate over each page to gather informations about memory + * usage which can be slow depending on the program memory allocations. + */ + (): MemoryUsage; + /** + * method returns an integer representing the Resident Set Size (RSS) in bytes. + */ + rss(): number; + } + interface MemoryUsage { + /** + * Resident Set Size, is the amount of space occupied in the main memory device (that is a subset of the total allocated memory) for the + * process, including all C++ and JavaScript objects and code. + */ + rss: number; + /** + * Refers to V8's memory usage. + */ + heapTotal: number; + /** + * Refers to V8's memory usage. + */ + heapUsed: number; + external: number; + /** + * Refers to memory allocated for `ArrayBuffer`s and `SharedArrayBuffer`s, including all Node.js Buffers. This is also included + * in the external value. When Node.js is used as an embedded library, this value may be `0` because allocations for `ArrayBuffer`s + * may not be tracked in that case. + */ + arrayBuffers: number; + } + interface CpuUsage { + user: number; + system: number; + } + interface ProcessRelease { + name: string; + sourceUrl?: string | undefined; + headersUrl?: string | undefined; + libUrl?: string | undefined; + lts?: string | undefined; + } + interface ProcessFeatures { + /** + * A boolean value that is `true` if the current Node.js build is caching builtin modules. + * @since v12.0.0 + */ + readonly cached_builtins: boolean; + /** + * A boolean value that is `true` if the current Node.js build is a debug build. + * @since v0.5.5 + */ + readonly debug: boolean; + /** + * A boolean value that is `true` if the current Node.js build includes the inspector. + * @since v11.10.0 + */ + readonly inspector: boolean; + /** + * A boolean value that is `true` if the current Node.js build includes support for IPv6. + * + * Since all Node.js builds have IPv6 support, this value is always `true`. + * @since v0.5.3 + * @deprecated This property is always true, and any checks based on it are redundant. + */ + readonly ipv6: boolean; + /** + * A boolean value that is `true` if the current Node.js build supports + * [loading ECMAScript modules using `require()`](https://nodejs.org/docs/latest-v22.x/api/modules.md#loading-ecmascript-modules-using-require). + * @since v22.10.0 + */ + readonly require_module: boolean; + /** + * A boolean value that is `true` if the current Node.js build includes support for TLS. + * @since v0.5.3 + */ + readonly tls: boolean; + /** + * A boolean value that is `true` if the current Node.js build includes support for ALPN in TLS. + * + * In Node.js 11.0.0 and later versions, the OpenSSL dependencies feature unconditional ALPN support. + * This value is therefore identical to that of `process.features.tls`. + * @since v4.8.0 + * @deprecated Use `process.features.tls` instead. + */ + readonly tls_alpn: boolean; + /** + * A boolean value that is `true` if the current Node.js build includes support for OCSP in TLS. + * + * In Node.js 11.0.0 and later versions, the OpenSSL dependencies feature unconditional OCSP support. + * This value is therefore identical to that of `process.features.tls`. + * @since v0.11.13 + * @deprecated Use `process.features.tls` instead. + */ + readonly tls_ocsp: boolean; + /** + * A boolean value that is `true` if the current Node.js build includes support for SNI in TLS. + * + * In Node.js 11.0.0 and later versions, the OpenSSL dependencies feature unconditional SNI support. + * This value is therefore identical to that of `process.features.tls`. + * @since v0.5.3 + * @deprecated Use `process.features.tls` instead. + */ + readonly tls_sni: boolean; + /** + * A value that is `"strip"` by default, + * `"transform"` if Node.js is run with `--experimental-transform-types`, and `false` if + * Node.js is run with `--no-experimental-strip-types`. + * @since v22.10.0 + */ + readonly typescript: "strip" | "transform" | false; + /** + * A boolean value that is `true` if the current Node.js build includes support for libuv. + * + * Since it's not possible to build Node.js without libuv, this value is always `true`. + * @since v0.5.3 + * @deprecated This property is always true, and any checks based on it are redundant. + */ + readonly uv: boolean; + } + interface ProcessVersions extends Dict { + http_parser: string; + node: string; + v8: string; + ares: string; + uv: string; + zlib: string; + modules: string; + openssl: string; + } + type Platform = + | "aix" + | "android" + | "darwin" + | "freebsd" + | "haiku" + | "linux" + | "openbsd" + | "sunos" + | "win32" + | "cygwin" + | "netbsd"; + type Architecture = + | "arm" + | "arm64" + | "ia32" + | "loong64" + | "mips" + | "mipsel" + | "ppc" + | "ppc64" + | "riscv64" + | "s390" + | "s390x" + | "x64"; + type Signals = + | "SIGABRT" + | "SIGALRM" + | "SIGBUS" + | "SIGCHLD" + | "SIGCONT" + | "SIGFPE" + | "SIGHUP" + | "SIGILL" + | "SIGINT" + | "SIGIO" + | "SIGIOT" + | "SIGKILL" + | "SIGPIPE" + | "SIGPOLL" + | "SIGPROF" + | "SIGPWR" + | "SIGQUIT" + | "SIGSEGV" + | "SIGSTKFLT" + | "SIGSTOP" + | "SIGSYS" + | "SIGTERM" + | "SIGTRAP" + | "SIGTSTP" + | "SIGTTIN" + | "SIGTTOU" + | "SIGUNUSED" + | "SIGURG" + | "SIGUSR1" + | "SIGUSR2" + | "SIGVTALRM" + | "SIGWINCH" + | "SIGXCPU" + | "SIGXFSZ" + | "SIGBREAK" + | "SIGLOST" + | "SIGINFO"; + type UncaughtExceptionOrigin = "uncaughtException" | "unhandledRejection"; + type MultipleResolveType = "resolve" | "reject"; + type BeforeExitListener = (code: number) => void; + type DisconnectListener = () => void; + type ExitListener = (code: number) => void; + type RejectionHandledListener = (promise: Promise) => void; + type UncaughtExceptionListener = (error: Error, origin: UncaughtExceptionOrigin) => void; + /** + * Most of the time the unhandledRejection will be an Error, but this should not be relied upon + * as *anything* can be thrown/rejected, it is therefore unsafe to assume that the value is an Error. + */ + type UnhandledRejectionListener = (reason: unknown, promise: Promise) => void; + type WarningListener = (warning: Error) => void; + type MessageListener = (message: unknown, sendHandle: SendHandle) => void; + type SignalsListener = (signal: Signals) => void; + type MultipleResolveListener = ( + type: MultipleResolveType, + promise: Promise, + value: unknown, + ) => void; + type WorkerListener = (worker: Worker) => void; + interface Socket extends ReadWriteStream { + isTTY?: true | undefined; + } + // Alias for compatibility + interface ProcessEnv extends Dict {} + interface HRTime { + /** + * This is the legacy version of {@link process.hrtime.bigint()} + * before bigint was introduced in JavaScript. + * + * The `process.hrtime()` method returns the current high-resolution real time in a `[seconds, nanoseconds]` tuple `Array`, + * where `nanoseconds` is the remaining part of the real time that can't be represented in second precision. + * + * `time` is an optional parameter that must be the result of a previous `process.hrtime()` call to diff with the current time. + * If the parameter passed in is not a tuple `Array`, a TypeError will be thrown. + * Passing in a user-defined array instead of the result of a previous call to `process.hrtime()` will lead to undefined behavior. + * + * These times are relative to an arbitrary time in the past, + * and not related to the time of day and therefore not subject to clock drift. + * The primary use is for measuring performance between intervals: + * ```js + * const { hrtime } = require('node:process'); + * const NS_PER_SEC = 1e9; + * const time = hrtime(); + * // [ 1800216, 25 ] + * + * setTimeout(() => { + * const diff = hrtime(time); + * // [ 1, 552 ] + * + * console.log(`Benchmark took ${diff[0] * NS_PER_SEC + diff[1]} nanoseconds`); + * // Benchmark took 1000000552 nanoseconds + * }, 1000); + * ``` + * @since 0.7.6 + * @legacy Use {@link process.hrtime.bigint()} instead. + * @param time The result of a previous call to `process.hrtime()` + */ + (time?: [number, number]): [number, number]; + /** + * The `bigint` version of the {@link process.hrtime()} method returning the current high-resolution real time in nanoseconds as a `bigint`. + * + * Unlike {@link process.hrtime()}, it does not support an additional time argument since the difference can just be computed directly by subtraction of the two `bigint`s. + * ```js + * import { hrtime } from 'node:process'; + * + * const start = hrtime.bigint(); + * // 191051479007711n + * + * setTimeout(() => { + * const end = hrtime.bigint(); + * // 191052633396993n + * + * console.log(`Benchmark took ${end - start} nanoseconds`); + * // Benchmark took 1154389282 nanoseconds + * }, 1000); + * ``` + * @since v10.7.0 + */ + bigint(): bigint; + } + interface ProcessPermission { + /** + * Verifies that the process is able to access the given scope and reference. + * If no reference is provided, a global scope is assumed, for instance, `process.permission.has('fs.read')` + * will check if the process has ALL file system read permissions. + * + * The reference has a meaning based on the provided scope. For example, the reference when the scope is File System means files and folders. + * + * The available scopes are: + * + * * `fs` - All File System + * * `fs.read` - File System read operations + * * `fs.write` - File System write operations + * * `child` - Child process spawning operations + * * `worker` - Worker thread spawning operation + * + * ```js + * // Check if the process has permission to read the README file + * process.permission.has('fs.read', './README.md'); + * // Check if the process has read permission operations + * process.permission.has('fs.read'); + * ``` + * @since v20.0.0 + */ + has(scope: string, reference?: string): boolean; + } + interface ProcessReport { + /** + * Write reports in a compact format, single-line JSON, more easily consumable by log processing systems + * than the default multi-line format designed for human consumption. + * @since v13.12.0, v12.17.0 + */ + compact: boolean; + /** + * Directory where the report is written. + * The default value is the empty string, indicating that reports are written to the current + * working directory of the Node.js process. + */ + directory: string; + /** + * Filename where the report is written. If set to the empty string, the output filename will be comprised + * of a timestamp, PID, and sequence number. The default value is the empty string. + */ + filename: string; + /** + * Returns a JavaScript Object representation of a diagnostic report for the running process. + * The report's JavaScript stack trace is taken from `err`, if present. + */ + getReport(err?: Error): object; + /** + * If true, a diagnostic report is generated on fatal errors, + * such as out of memory errors or failed C++ assertions. + * @default false + */ + reportOnFatalError: boolean; + /** + * If true, a diagnostic report is generated when the process + * receives the signal specified by process.report.signal. + * @default false + */ + reportOnSignal: boolean; + /** + * If true, a diagnostic report is generated on uncaught exception. + * @default false + */ + reportOnUncaughtException: boolean; + /** + * If true, a diagnostic report is generated without the environment variables. + * @default false + */ + excludeEnv: boolean; + /** + * The signal used to trigger the creation of a diagnostic report. + * @default 'SIGUSR2' + */ + signal: Signals; + /** + * Writes a diagnostic report to a file. If filename is not provided, the default filename + * includes the date, time, PID, and a sequence number. + * The report's JavaScript stack trace is taken from `err`, if present. + * + * If the value of filename is set to `'stdout'` or `'stderr'`, the report is written + * to the stdout or stderr of the process respectively. + * @param fileName Name of the file where the report is written. + * This should be a relative path, that will be appended to the directory specified in + * `process.report.directory`, or the current working directory of the Node.js process, + * if unspecified. + * @param err A custom error used for reporting the JavaScript stack. + * @return Filename of the generated report. + */ + writeReport(fileName?: string, err?: Error): string; + writeReport(err?: Error): string; + } + interface ResourceUsage { + fsRead: number; + fsWrite: number; + involuntaryContextSwitches: number; + ipcReceived: number; + ipcSent: number; + majorPageFault: number; + maxRSS: number; + minorPageFault: number; + sharedMemorySize: number; + signalsCount: number; + swappedOut: number; + systemCPUTime: number; + unsharedDataSize: number; + unsharedStackSize: number; + userCPUTime: number; + voluntaryContextSwitches: number; + } + interface EmitWarningOptions { + /** + * When `warning` is a `string`, `type` is the name to use for the _type_ of warning being emitted. + * + * @default 'Warning' + */ + type?: string | undefined; + /** + * A unique identifier for the warning instance being emitted. + */ + code?: string | undefined; + /** + * When `warning` is a `string`, `ctor` is an optional function used to limit the generated stack trace. + * + * @default process.emitWarning + */ + ctor?: Function | undefined; + /** + * Additional text to include with the error. + */ + detail?: string | undefined; + } + interface ProcessConfig { + readonly target_defaults: { + readonly cflags: any[]; + readonly default_configuration: string; + readonly defines: string[]; + readonly include_dirs: string[]; + readonly libraries: string[]; + }; + readonly variables: { + readonly clang: number; + readonly host_arch: string; + readonly node_install_npm: boolean; + readonly node_install_waf: boolean; + readonly node_prefix: string; + readonly node_shared_openssl: boolean; + readonly node_shared_v8: boolean; + readonly node_shared_zlib: boolean; + readonly node_use_dtrace: boolean; + readonly node_use_etw: boolean; + readonly node_use_openssl: boolean; + readonly target_arch: string; + readonly v8_no_strict_aliasing: number; + readonly v8_use_snapshot: boolean; + readonly visibility: string; + }; + } + interface Process extends EventEmitter { + /** + * The `process.stdout` property returns a stream connected to`stdout` (fd `1`). It is a `net.Socket` (which is a `Duplex` stream) unless fd `1` refers to a file, in which case it is + * a `Writable` stream. + * + * For example, to copy `process.stdin` to `process.stdout`: + * + * ```js + * import { stdin, stdout } from 'node:process'; + * + * stdin.pipe(stdout); + * ``` + * + * `process.stdout` differs from other Node.js streams in important ways. See `note on process I/O` for more information. + */ + stdout: WriteStream & { + fd: 1; + }; + /** + * The `process.stderr` property returns a stream connected to`stderr` (fd `2`). It is a `net.Socket` (which is a `Duplex` stream) unless fd `2` refers to a file, in which case it is + * a `Writable` stream. + * + * `process.stderr` differs from other Node.js streams in important ways. See `note on process I/O` for more information. + */ + stderr: WriteStream & { + fd: 2; + }; + /** + * The `process.stdin` property returns a stream connected to`stdin` (fd `0`). It is a `net.Socket` (which is a `Duplex` stream) unless fd `0` refers to a file, in which case it is + * a `Readable` stream. + * + * For details of how to read from `stdin` see `readable.read()`. + * + * As a `Duplex` stream, `process.stdin` can also be used in "old" mode that + * is compatible with scripts written for Node.js prior to v0.10\. + * For more information see `Stream compatibility`. + * + * In "old" streams mode the `stdin` stream is paused by default, so one + * must call `process.stdin.resume()` to read from it. Note also that calling `process.stdin.resume()` itself would switch stream to "old" mode. + */ + stdin: ReadStream & { + fd: 0; + }; + /** + * The `process.argv` property returns an array containing the command-line + * arguments passed when the Node.js process was launched. The first element will + * be {@link execPath}. See `process.argv0` if access to the original value + * of `argv[0]` is needed. The second element will be the path to the JavaScript + * file being executed. The remaining elements will be any additional command-line + * arguments. + * + * For example, assuming the following script for `process-args.js`: + * + * ```js + * import { argv } from 'node:process'; + * + * // print process.argv + * argv.forEach((val, index) => { + * console.log(`${index}: ${val}`); + * }); + * ``` + * + * Launching the Node.js process as: + * + * ```bash + * node process-args.js one two=three four + * ``` + * + * Would generate the output: + * + * ```text + * 0: /usr/local/bin/node + * 1: /Users/mjr/work/node/process-args.js + * 2: one + * 3: two=three + * 4: four + * ``` + * @since v0.1.27 + */ + argv: string[]; + /** + * The `process.argv0` property stores a read-only copy of the original value of`argv[0]` passed when Node.js starts. + * + * ```console + * $ bash -c 'exec -a customArgv0 ./node' + * > process.argv[0] + * '/Volumes/code/external/node/out/Release/node' + * > process.argv0 + * 'customArgv0' + * ``` + * @since v6.4.0 + */ + argv0: string; + /** + * The `process.execArgv` property returns the set of Node.js-specific command-line + * options passed when the Node.js process was launched. These options do not + * appear in the array returned by the {@link argv} property, and do not + * include the Node.js executable, the name of the script, or any options following + * the script name. These options are useful in order to spawn child processes with + * the same execution environment as the parent. + * + * ```bash + * node --icu-data-dir=./foo --require ./bar.js script.js --version + * ``` + * + * Results in `process.execArgv`: + * + * ```js + * ["--icu-data-dir=./foo", "--require", "./bar.js"] + * ``` + * + * And `process.argv`: + * + * ```js + * ['/usr/local/bin/node', 'script.js', '--version'] + * ``` + * + * Refer to `Worker constructor` for the detailed behavior of worker + * threads with this property. + * @since v0.7.7 + */ + execArgv: string[]; + /** + * The `process.execPath` property returns the absolute pathname of the executable + * that started the Node.js process. Symbolic links, if any, are resolved. + * + * ```js + * '/usr/local/bin/node' + * ``` + * @since v0.1.100 + */ + execPath: string; + /** + * The `process.abort()` method causes the Node.js process to exit immediately and + * generate a core file. + * + * This feature is not available in `Worker` threads. + * @since v0.7.0 + */ + abort(): never; + /** + * The `process.chdir()` method changes the current working directory of the + * Node.js process or throws an exception if doing so fails (for instance, if + * the specified `directory` does not exist). + * + * ```js + * import { chdir, cwd } from 'node:process'; + * + * console.log(`Starting directory: ${cwd()}`); + * try { + * chdir('/tmp'); + * console.log(`New directory: ${cwd()}`); + * } catch (err) { + * console.error(`chdir: ${err}`); + * } + * ``` + * + * This feature is not available in `Worker` threads. + * @since v0.1.17 + */ + chdir(directory: string): void; + /** + * The `process.cwd()` method returns the current working directory of the Node.js + * process. + * + * ```js + * import { cwd } from 'node:process'; + * + * console.log(`Current directory: ${cwd()}`); + * ``` + * @since v0.1.8 + */ + cwd(): string; + /** + * The port used by the Node.js debugger when enabled. + * + * ```js + * import process from 'node:process'; + * + * process.debugPort = 5858; + * ``` + * @since v0.7.2 + */ + debugPort: number; + /** + * The `process.dlopen()` method allows dynamically loading shared objects. It is primarily used by `require()` to load C++ Addons, and + * should not be used directly, except in special cases. In other words, `require()` should be preferred over `process.dlopen()` + * unless there are specific reasons such as custom dlopen flags or loading from ES modules. + * + * The `flags` argument is an integer that allows to specify dlopen behavior. See the `[os.constants.dlopen](https://nodejs.org/docs/latest-v22.x/api/os.html#dlopen-constants)` + * documentation for details. + * + * An important requirement when calling `process.dlopen()` is that the `module` instance must be passed. Functions exported by the C++ Addon + * are then accessible via `module.exports`. + * + * The example below shows how to load a C++ Addon, named `local.node`, that exports a `foo` function. All the symbols are loaded before the call returns, by passing the `RTLD_NOW` constant. + * In this example the constant is assumed to be available. + * + * ```js + * import { dlopen } from 'node:process'; + * import { constants } from 'node:os'; + * import { fileURLToPath } from 'node:url'; + * + * const module = { exports: {} }; + * dlopen(module, fileURLToPath(new URL('local.node', import.meta.url)), + * constants.dlopen.RTLD_NOW); + * module.exports.foo(); + * ``` + */ + dlopen(module: object, filename: string, flags?: number): void; + /** + * The `process.emitWarning()` method can be used to emit custom or application + * specific process warnings. These can be listened for by adding a handler to the `'warning'` event. + * + * ```js + * import { emitWarning } from 'node:process'; + * + * // Emit a warning using a string. + * emitWarning('Something happened!'); + * // Emits: (node: 56338) Warning: Something happened! + * ``` + * + * ```js + * import { emitWarning } from 'node:process'; + * + * // Emit a warning using a string and a type. + * emitWarning('Something Happened!', 'CustomWarning'); + * // Emits: (node:56338) CustomWarning: Something Happened! + * ``` + * + * ```js + * import { emitWarning } from 'node:process'; + * + * emitWarning('Something happened!', 'CustomWarning', 'WARN001'); + * // Emits: (node:56338) [WARN001] CustomWarning: Something happened! + * ```js + * + * In each of the previous examples, an `Error` object is generated internally by `process.emitWarning()` and passed through to the `'warning'` handler. + * + * ```js + * import process from 'node:process'; + * + * process.on('warning', (warning) => { + * console.warn(warning.name); // 'Warning' + * console.warn(warning.message); // 'Something happened!' + * console.warn(warning.code); // 'MY_WARNING' + * console.warn(warning.stack); // Stack trace + * console.warn(warning.detail); // 'This is some additional information' + * }); + * ``` + * + * If `warning` is passed as an `Error` object, it will be passed through to the `'warning'` event handler + * unmodified (and the optional `type`, `code` and `ctor` arguments will be ignored): + * + * ```js + * import { emitWarning } from 'node:process'; + * + * // Emit a warning using an Error object. + * const myWarning = new Error('Something happened!'); + * // Use the Error name property to specify the type name + * myWarning.name = 'CustomWarning'; + * myWarning.code = 'WARN001'; + * + * emitWarning(myWarning); + * // Emits: (node:56338) [WARN001] CustomWarning: Something happened! + * ``` + * + * A `TypeError` is thrown if `warning` is anything other than a string or `Error` object. + * + * While process warnings use `Error` objects, the process warning mechanism is not a replacement for normal error handling mechanisms. + * + * The following additional handling is implemented if the warning `type` is `'DeprecationWarning'`: + * * If the `--throw-deprecation` command-line flag is used, the deprecation warning is thrown as an exception rather than being emitted as an event. + * * If the `--no-deprecation` command-line flag is used, the deprecation warning is suppressed. + * * If the `--trace-deprecation` command-line flag is used, the deprecation warning is printed to `stderr` along with the full stack trace. + * @since v8.0.0 + * @param warning The warning to emit. + */ + emitWarning(warning: string | Error, ctor?: Function): void; + emitWarning(warning: string | Error, type?: string, ctor?: Function): void; + emitWarning(warning: string | Error, type?: string, code?: string, ctor?: Function): void; + emitWarning(warning: string | Error, options?: EmitWarningOptions): void; + /** + * The `process.env` property returns an object containing the user environment. + * See [`environ(7)`](http://man7.org/linux/man-pages/man7/environ.7.html). + * + * An example of this object looks like: + * + * ```js + * { + * TERM: 'xterm-256color', + * SHELL: '/usr/local/bin/bash', + * USER: 'maciej', + * PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin', + * PWD: '/Users/maciej', + * EDITOR: 'vim', + * SHLVL: '1', + * HOME: '/Users/maciej', + * LOGNAME: 'maciej', + * _: '/usr/local/bin/node' + * } + * ``` + * + * It is possible to modify this object, but such modifications will not be + * reflected outside the Node.js process, or (unless explicitly requested) + * to other `Worker` threads. + * In other words, the following example would not work: + * + * ```bash + * node -e 'process.env.foo = "bar"' && echo $foo + * ``` + * + * While the following will: + * + * ```js + * import { env } from 'node:process'; + * + * env.foo = 'bar'; + * console.log(env.foo); + * ``` + * + * Assigning a property on `process.env` will implicitly convert the value + * to a string. **This behavior is deprecated.** Future versions of Node.js may + * throw an error when the value is not a string, number, or boolean. + * + * ```js + * import { env } from 'node:process'; + * + * env.test = null; + * console.log(env.test); + * // => 'null' + * env.test = undefined; + * console.log(env.test); + * // => 'undefined' + * ``` + * + * Use `delete` to delete a property from `process.env`. + * + * ```js + * import { env } from 'node:process'; + * + * env.TEST = 1; + * delete env.TEST; + * console.log(env.TEST); + * // => undefined + * ``` + * + * On Windows operating systems, environment variables are case-insensitive. + * + * ```js + * import { env } from 'node:process'; + * + * env.TEST = 1; + * console.log(env.test); + * // => 1 + * ``` + * + * Unless explicitly specified when creating a `Worker` instance, + * each `Worker` thread has its own copy of `process.env`, based on its + * parent thread's `process.env`, or whatever was specified as the `env` option + * to the `Worker` constructor. Changes to `process.env` will not be visible + * across `Worker` threads, and only the main thread can make changes that + * are visible to the operating system or to native add-ons. On Windows, a copy of `process.env` on a `Worker` instance operates in a case-sensitive manner + * unlike the main thread. + * @since v0.1.27 + */ + env: ProcessEnv; + /** + * The `process.exit()` method instructs Node.js to terminate the process + * synchronously with an exit status of `code`. If `code` is omitted, exit uses + * either the 'success' code `0` or the value of `process.exitCode` if it has been + * set. Node.js will not terminate until all the `'exit'` event listeners are + * called. + * + * To exit with a 'failure' code: + * + * ```js + * import { exit } from 'node:process'; + * + * exit(1); + * ``` + * + * The shell that executed Node.js should see the exit code as `1`. + * + * Calling `process.exit()` will force the process to exit as quickly as possible + * even if there are still asynchronous operations pending that have not yet + * completed fully, including I/O operations to `process.stdout` and `process.stderr`. + * + * In most situations, it is not actually necessary to call `process.exit()` explicitly. The Node.js process will exit on its own _if there is no additional_ + * _work pending_ in the event loop. The `process.exitCode` property can be set to + * tell the process which exit code to use when the process exits gracefully. + * + * For instance, the following example illustrates a _misuse_ of the `process.exit()` method that could lead to data printed to stdout being + * truncated and lost: + * + * ```js + * import { exit } from 'node:process'; + * + * // This is an example of what *not* to do: + * if (someConditionNotMet()) { + * printUsageToStdout(); + * exit(1); + * } + * ``` + * + * The reason this is problematic is because writes to `process.stdout` in Node.js + * are sometimes _asynchronous_ and may occur over multiple ticks of the Node.js + * event loop. Calling `process.exit()`, however, forces the process to exit _before_ those additional writes to `stdout` can be performed. + * + * Rather than calling `process.exit()` directly, the code _should_ set the `process.exitCode` and allow the process to exit naturally by avoiding + * scheduling any additional work for the event loop: + * + * ```js + * import process from 'node:process'; + * + * // How to properly set the exit code while letting + * // the process exit gracefully. + * if (someConditionNotMet()) { + * printUsageToStdout(); + * process.exitCode = 1; + * } + * ``` + * + * If it is necessary to terminate the Node.js process due to an error condition, + * throwing an _uncaught_ error and allowing the process to terminate accordingly + * is safer than calling `process.exit()`. + * + * In `Worker` threads, this function stops the current thread rather + * than the current process. + * @since v0.1.13 + * @param [code=0] The exit code. For string type, only integer strings (e.g.,'1') are allowed. + */ + exit(code?: number | string | null): never; + /** + * A number which will be the process exit code, when the process either + * exits gracefully, or is exited via {@link exit} without specifying + * a code. + * + * Specifying a code to {@link exit} will override any + * previous setting of `process.exitCode`. + * @default undefined + * @since v0.11.8 + */ + exitCode: number | string | null | undefined; + finalization: { + /** + * This function registers a callback to be called when the process emits the `exit` event if the `ref` object was not garbage collected. + * If the object `ref` was garbage collected before the `exit` event is emitted, the callback will be removed from the finalization registry, and it will not be called on process exit. + * + * Inside the callback you can release the resources allocated by the `ref` object. + * Be aware that all limitations applied to the `beforeExit` event are also applied to the callback function, + * this means that there is a possibility that the callback will not be called under special circumstances. + * + * The idea of ​​this function is to help you free up resources when the starts process exiting, but also let the object be garbage collected if it is no longer being used. + * @param ref The reference to the resource that is being tracked. + * @param callback The callback function to be called when the resource is finalized. + * @since v22.5.0 + * @experimental + */ + register(ref: T, callback: (ref: T, event: "exit") => void): void; + /** + * This function behaves exactly like the `register`, except that the callback will be called when the process emits the `beforeExit` event if `ref` object was not garbage collected. + * + * Be aware that all limitations applied to the `beforeExit` event are also applied to the callback function, this means that there is a possibility that the callback will not be called under special circumstances. + * @param ref The reference to the resource that is being tracked. + * @param callback The callback function to be called when the resource is finalized. + * @since v22.5.0 + * @experimental + */ + registerBeforeExit(ref: T, callback: (ref: T, event: "beforeExit") => void): void; + /** + * This function remove the register of the object from the finalization registry, so the callback will not be called anymore. + * @param ref The reference to the resource that was registered previously. + * @since v22.5.0 + * @experimental + */ + unregister(ref: object): void; + }; + /** + * The `process.getActiveResourcesInfo()` method returns an array of strings containing + * the types of the active resources that are currently keeping the event loop alive. + * + * ```js + * import { getActiveResourcesInfo } from 'node:process'; + * import { setTimeout } from 'node:timers'; + + * console.log('Before:', getActiveResourcesInfo()); + * setTimeout(() => {}, 1000); + * console.log('After:', getActiveResourcesInfo()); + * // Prints: + * // Before: [ 'TTYWrap', 'TTYWrap', 'TTYWrap' ] + * // After: [ 'TTYWrap', 'TTYWrap', 'TTYWrap', 'Timeout' ] + * ``` + * @since v17.3.0, v16.14.0 + */ + getActiveResourcesInfo(): string[]; + /** + * Provides a way to load built-in modules in a globally available function. + * @param id ID of the built-in module being requested. + */ + getBuiltinModule(id: ID): BuiltInModule[ID]; + getBuiltinModule(id: string): object | undefined; + /** + * The `process.getgid()` method returns the numerical group identity of the + * process. (See [`getgid(2)`](http://man7.org/linux/man-pages/man2/getgid.2.html).) + * + * ```js + * import process from 'node:process'; + * + * if (process.getgid) { + * console.log(`Current gid: ${process.getgid()}`); + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * @since v0.1.31 + */ + getgid?: () => number; + /** + * The `process.setgid()` method sets the group identity of the process. (See [`setgid(2)`](http://man7.org/linux/man-pages/man2/setgid.2.html).) The `id` can be passed as either a + * numeric ID or a group name + * string. If a group name is specified, this method blocks while resolving the + * associated numeric ID. + * + * ```js + * import process from 'node:process'; + * + * if (process.getgid && process.setgid) { + * console.log(`Current gid: ${process.getgid()}`); + * try { + * process.setgid(501); + * console.log(`New gid: ${process.getgid()}`); + * } catch (err) { + * console.log(`Failed to set gid: ${err}`); + * } + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * This feature is not available in `Worker` threads. + * @since v0.1.31 + * @param id The group name or ID + */ + setgid?: (id: number | string) => void; + /** + * The `process.getuid()` method returns the numeric user identity of the process. + * (See [`getuid(2)`](http://man7.org/linux/man-pages/man2/getuid.2.html).) + * + * ```js + * import process from 'node:process'; + * + * if (process.getuid) { + * console.log(`Current uid: ${process.getuid()}`); + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * @since v0.1.28 + */ + getuid?: () => number; + /** + * The `process.setuid(id)` method sets the user identity of the process. (See [`setuid(2)`](http://man7.org/linux/man-pages/man2/setuid.2.html).) The `id` can be passed as either a + * numeric ID or a username string. + * If a username is specified, the method blocks while resolving the associated + * numeric ID. + * + * ```js + * import process from 'node:process'; + * + * if (process.getuid && process.setuid) { + * console.log(`Current uid: ${process.getuid()}`); + * try { + * process.setuid(501); + * console.log(`New uid: ${process.getuid()}`); + * } catch (err) { + * console.log(`Failed to set uid: ${err}`); + * } + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * This feature is not available in `Worker` threads. + * @since v0.1.28 + */ + setuid?: (id: number | string) => void; + /** + * The `process.geteuid()` method returns the numerical effective user identity of + * the process. (See [`geteuid(2)`](http://man7.org/linux/man-pages/man2/geteuid.2.html).) + * + * ```js + * import process from 'node:process'; + * + * if (process.geteuid) { + * console.log(`Current uid: ${process.geteuid()}`); + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * @since v2.0.0 + */ + geteuid?: () => number; + /** + * The `process.seteuid()` method sets the effective user identity of the process. + * (See [`seteuid(2)`](http://man7.org/linux/man-pages/man2/seteuid.2.html).) The `id` can be passed as either a numeric ID or a username + * string. If a username is specified, the method blocks while resolving the + * associated numeric ID. + * + * ```js + * import process from 'node:process'; + * + * if (process.geteuid && process.seteuid) { + * console.log(`Current uid: ${process.geteuid()}`); + * try { + * process.seteuid(501); + * console.log(`New uid: ${process.geteuid()}`); + * } catch (err) { + * console.log(`Failed to set uid: ${err}`); + * } + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * This feature is not available in `Worker` threads. + * @since v2.0.0 + * @param id A user name or ID + */ + seteuid?: (id: number | string) => void; + /** + * The `process.getegid()` method returns the numerical effective group identity + * of the Node.js process. (See [`getegid(2)`](http://man7.org/linux/man-pages/man2/getegid.2.html).) + * + * ```js + * import process from 'node:process'; + * + * if (process.getegid) { + * console.log(`Current gid: ${process.getegid()}`); + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * @since v2.0.0 + */ + getegid?: () => number; + /** + * The `process.setegid()` method sets the effective group identity of the process. + * (See [`setegid(2)`](http://man7.org/linux/man-pages/man2/setegid.2.html).) The `id` can be passed as either a numeric ID or a group + * name string. If a group name is specified, this method blocks while resolving + * the associated a numeric ID. + * + * ```js + * import process from 'node:process'; + * + * if (process.getegid && process.setegid) { + * console.log(`Current gid: ${process.getegid()}`); + * try { + * process.setegid(501); + * console.log(`New gid: ${process.getegid()}`); + * } catch (err) { + * console.log(`Failed to set gid: ${err}`); + * } + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * This feature is not available in `Worker` threads. + * @since v2.0.0 + * @param id A group name or ID + */ + setegid?: (id: number | string) => void; + /** + * The `process.getgroups()` method returns an array with the supplementary group + * IDs. POSIX leaves it unspecified if the effective group ID is included but + * Node.js ensures it always is. + * + * ```js + * import process from 'node:process'; + * + * if (process.getgroups) { + * console.log(process.getgroups()); // [ 16, 21, 297 ] + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * @since v0.9.4 + */ + getgroups?: () => number[]; + /** + * The `process.setgroups()` method sets the supplementary group IDs for the + * Node.js process. This is a privileged operation that requires the Node.js + * process to have `root` or the `CAP_SETGID` capability. + * + * The `groups` array can contain numeric group IDs, group names, or both. + * + * ```js + * import process from 'node:process'; + * + * if (process.getgroups && process.setgroups) { + * try { + * process.setgroups([501]); + * console.log(process.getgroups()); // new groups + * } catch (err) { + * console.log(`Failed to set groups: ${err}`); + * } + * } + * ``` + * + * This function is only available on POSIX platforms (i.e. not Windows or + * Android). + * This feature is not available in `Worker` threads. + * @since v0.9.4 + */ + setgroups?: (groups: ReadonlyArray) => void; + /** + * The `process.setUncaughtExceptionCaptureCallback()` function sets a function + * that will be invoked when an uncaught exception occurs, which will receive the + * exception value itself as its first argument. + * + * If such a function is set, the `'uncaughtException'` event will + * not be emitted. If `--abort-on-uncaught-exception` was passed from the + * command line or set through `v8.setFlagsFromString()`, the process will + * not abort. Actions configured to take place on exceptions such as report + * generations will be affected too + * + * To unset the capture function, `process.setUncaughtExceptionCaptureCallback(null)` may be used. Calling this + * method with a non-`null` argument while another capture function is set will + * throw an error. + * + * Using this function is mutually exclusive with using the deprecated `domain` built-in module. + * @since v9.3.0 + */ + setUncaughtExceptionCaptureCallback(cb: ((err: Error) => void) | null): void; + /** + * Indicates whether a callback has been set using {@link setUncaughtExceptionCaptureCallback}. + * @since v9.3.0 + */ + hasUncaughtExceptionCaptureCallback(): boolean; + /** + * The `process.sourceMapsEnabled` property returns whether the [Source Map v3](https://sourcemaps.info/spec.html) support for stack traces is enabled. + * @since v20.7.0 + * @experimental + */ + readonly sourceMapsEnabled: boolean; + /** + * This function enables or disables the [Source Map v3](https://sourcemaps.info/spec.html) support for + * stack traces. + * + * It provides same features as launching Node.js process with commandline options `--enable-source-maps`. + * + * Only source maps in JavaScript files that are loaded after source maps has been + * enabled will be parsed and loaded. + * @since v16.6.0, v14.18.0 + * @experimental + */ + setSourceMapsEnabled(value: boolean): void; + /** + * The `process.version` property contains the Node.js version string. + * + * ```js + * import { version } from 'node:process'; + * + * console.log(`Version: ${version}`); + * // Version: v14.8.0 + * ``` + * + * To get the version string without the prepended _v_, use`process.versions.node`. + * @since v0.1.3 + */ + readonly version: string; + /** + * The `process.versions` property returns an object listing the version strings of + * Node.js and its dependencies. `process.versions.modules` indicates the current + * ABI version, which is increased whenever a C++ API changes. Node.js will refuse + * to load modules that were compiled against a different module ABI version. + * + * ```js + * import { versions } from 'node:process'; + * + * console.log(versions); + * ``` + * + * Will generate an object similar to: + * + * ```console + * { node: '20.2.0', + * acorn: '8.8.2', + * ada: '2.4.0', + * ares: '1.19.0', + * base64: '0.5.0', + * brotli: '1.0.9', + * cjs_module_lexer: '1.2.2', + * cldr: '43.0', + * icu: '73.1', + * llhttp: '8.1.0', + * modules: '115', + * napi: '8', + * nghttp2: '1.52.0', + * nghttp3: '0.7.0', + * ngtcp2: '0.8.1', + * openssl: '3.0.8+quic', + * simdutf: '3.2.9', + * tz: '2023c', + * undici: '5.22.0', + * unicode: '15.0', + * uv: '1.44.2', + * uvwasi: '0.0.16', + * v8: '11.3.244.8-node.9', + * zlib: '1.2.13' } + * ``` + * @since v0.2.0 + */ + readonly versions: ProcessVersions; + /** + * The `process.config` property returns a frozen `Object` containing the + * JavaScript representation of the configure options used to compile the current + * Node.js executable. This is the same as the `config.gypi` file that was produced + * when running the `./configure` script. + * + * An example of the possible output looks like: + * + * ```js + * { + * target_defaults: + * { cflags: [], + * default_configuration: 'Release', + * defines: [], + * include_dirs: [], + * libraries: [] }, + * variables: + * { + * host_arch: 'x64', + * napi_build_version: 5, + * node_install_npm: 'true', + * node_prefix: '', + * node_shared_cares: 'false', + * node_shared_http_parser: 'false', + * node_shared_libuv: 'false', + * node_shared_zlib: 'false', + * node_use_openssl: 'true', + * node_shared_openssl: 'false', + * strict_aliasing: 'true', + * target_arch: 'x64', + * v8_use_snapshot: 1 + * } + * } + * ``` + * @since v0.7.7 + */ + readonly config: ProcessConfig; + /** + * The `process.kill()` method sends the `signal` to the process identified by`pid`. + * + * Signal names are strings such as `'SIGINT'` or `'SIGHUP'`. See `Signal Events` and [`kill(2)`](http://man7.org/linux/man-pages/man2/kill.2.html) for more information. + * + * This method will throw an error if the target `pid` does not exist. As a special + * case, a signal of `0` can be used to test for the existence of a process. + * Windows platforms will throw an error if the `pid` is used to kill a process + * group. + * + * Even though the name of this function is `process.kill()`, it is really just a + * signal sender, like the `kill` system call. The signal sent may do something + * other than kill the target process. + * + * ```js + * import process, { kill } from 'node:process'; + * + * process.on('SIGHUP', () => { + * console.log('Got SIGHUP signal.'); + * }); + * + * setTimeout(() => { + * console.log('Exiting.'); + * process.exit(0); + * }, 100); + * + * kill(process.pid, 'SIGHUP'); + * ``` + * + * When `SIGUSR1` is received by a Node.js process, Node.js will start the + * debugger. See `Signal Events`. + * @since v0.0.6 + * @param pid A process ID + * @param [signal='SIGTERM'] The signal to send, either as a string or number. + */ + kill(pid: number, signal?: string | number): true; + /** + * Loads the environment configuration from a `.env` file into `process.env`. If + * the file is not found, error will be thrown. + * + * To load a specific .env file by specifying its path, use the following code: + * + * ```js + * import { loadEnvFile } from 'node:process'; + * + * loadEnvFile('./development.env') + * ``` + * @since v20.12.0 + * @param path The path to the .env file + */ + loadEnvFile(path?: PathLike): void; + /** + * The `process.pid` property returns the PID of the process. + * + * ```js + * import { pid } from 'node:process'; + * + * console.log(`This process is pid ${pid}`); + * ``` + * @since v0.1.15 + */ + readonly pid: number; + /** + * The `process.ppid` property returns the PID of the parent of the + * current process. + * + * ```js + * import { ppid } from 'node:process'; + * + * console.log(`The parent process is pid ${ppid}`); + * ``` + * @since v9.2.0, v8.10.0, v6.13.0 + */ + readonly ppid: number; + /** + * The `process.threadCpuUsage()` method returns the user and system CPU time usage of + * the current worker thread, in an object with properties `user` and `system`, whose + * values are microsecond values (millionth of a second). + * + * The result of a previous call to `process.threadCpuUsage()` can be passed as the + * argument to the function, to get a diff reading. + * @since v22.19.0 + * @param previousValue A previous return value from calling + * `process.threadCpuUsage()` + */ + threadCpuUsage(previousValue?: CpuUsage): CpuUsage; + /** + * The `process.title` property returns the current process title (i.e. returns + * the current value of `ps`). Assigning a new value to `process.title` modifies + * the current value of `ps`. + * + * When a new value is assigned, different platforms will impose different maximum + * length restrictions on the title. Usually such restrictions are quite limited. + * For instance, on Linux and macOS, `process.title` is limited to the size of the + * binary name plus the length of the command-line arguments because setting the `process.title` overwrites the `argv` memory of the process. Node.js v0.8 + * allowed for longer process title strings by also overwriting the `environ` memory but that was potentially insecure and confusing in some (rather obscure) + * cases. + * + * Assigning a value to `process.title` might not result in an accurate label + * within process manager applications such as macOS Activity Monitor or Windows + * Services Manager. + * @since v0.1.104 + */ + title: string; + /** + * The operating system CPU architecture for which the Node.js binary was compiled. + * Possible values are: `'arm'`, `'arm64'`, `'ia32'`, `'loong64'`, `'mips'`, `'mipsel'`, `'ppc'`, `'ppc64'`, `'riscv64'`, `'s390'`, `'s390x'`, and `'x64'`. + * + * ```js + * import { arch } from 'node:process'; + * + * console.log(`This processor architecture is ${arch}`); + * ``` + * @since v0.5.0 + */ + readonly arch: Architecture; + /** + * The `process.platform` property returns a string identifying the operating + * system platform for which the Node.js binary was compiled. + * + * Currently possible values are: + * + * * `'aix'` + * * `'darwin'` + * * `'freebsd'` + * * `'linux'` + * * `'openbsd'` + * * `'sunos'` + * * `'win32'` + * + * ```js + * import { platform } from 'node:process'; + * + * console.log(`This platform is ${platform}`); + * ``` + * + * The value `'android'` may also be returned if the Node.js is built on the + * Android operating system. However, Android support in Node.js [is experimental](https://github.com/nodejs/node/blob/HEAD/BUILDING.md#androidandroid-based-devices-eg-firefox-os). + * @since v0.1.16 + */ + readonly platform: Platform; + /** + * The `process.mainModule` property provides an alternative way of retrieving `require.main`. The difference is that if the main module changes at + * runtime, `require.main` may still refer to the original main module in + * modules that were required before the change occurred. Generally, it's + * safe to assume that the two refer to the same module. + * + * As with `require.main`, `process.mainModule` will be `undefined` if there + * is no entry script. + * @since v0.1.17 + * @deprecated Since v14.0.0 - Use `main` instead. + */ + mainModule?: Module; + memoryUsage: MemoryUsageFn; + /** + * Gets the amount of memory available to the process (in bytes) based on + * limits imposed by the OS. If there is no such constraint, or the constraint + * is unknown, `0` is returned. + * + * See [`uv_get_constrained_memory`](https://docs.libuv.org/en/v1.x/misc.html#c.uv_get_constrained_memory) for more + * information. + * @since v19.6.0, v18.15.0 + */ + constrainedMemory(): number; + /** + * Gets the amount of free memory that is still available to the process (in bytes). + * See [`uv_get_available_memory`](https://nodejs.org/docs/latest-v22.x/api/process.html#processavailablememory) for more information. + * @since v20.13.0 + */ + availableMemory(): number; + /** + * The `process.cpuUsage()` method returns the user and system CPU time usage of + * the current process, in an object with properties `user` and `system`, whose + * values are microsecond values (millionth of a second). These values measure time + * spent in user and system code respectively, and may end up being greater than + * actual elapsed time if multiple CPU cores are performing work for this process. + * + * The result of a previous call to `process.cpuUsage()` can be passed as the + * argument to the function, to get a diff reading. + * + * ```js + * import { cpuUsage } from 'node:process'; + * + * const startUsage = cpuUsage(); + * // { user: 38579, system: 6986 } + * + * // spin the CPU for 500 milliseconds + * const now = Date.now(); + * while (Date.now() - now < 500); + * + * console.log(cpuUsage(startUsage)); + * // { user: 514883, system: 11226 } + * ``` + * @since v6.1.0 + * @param previousValue A previous return value from calling `process.cpuUsage()` + */ + cpuUsage(previousValue?: CpuUsage): CpuUsage; + /** + * `process.nextTick()` adds `callback` to the "next tick queue". This queue is + * fully drained after the current operation on the JavaScript stack runs to + * completion and before the event loop is allowed to continue. It's possible to + * create an infinite loop if one were to recursively call `process.nextTick()`. + * See the [Event Loop](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#process-nexttick) guide for more background. + * + * ```js + * import { nextTick } from 'node:process'; + * + * console.log('start'); + * nextTick(() => { + * console.log('nextTick callback'); + * }); + * console.log('scheduled'); + * // Output: + * // start + * // scheduled + * // nextTick callback + * ``` + * + * This is important when developing APIs in order to give users the opportunity + * to assign event handlers _after_ an object has been constructed but before any + * I/O has occurred: + * + * ```js + * import { nextTick } from 'node:process'; + * + * function MyThing(options) { + * this.setupOptions(options); + * + * nextTick(() => { + * this.startDoingStuff(); + * }); + * } + * + * const thing = new MyThing(); + * thing.getReadyForStuff(); + * + * // thing.startDoingStuff() gets called now, not before. + * ``` + * + * It is very important for APIs to be either 100% synchronous or 100% + * asynchronous. Consider this example: + * + * ```js + * // WARNING! DO NOT USE! BAD UNSAFE HAZARD! + * function maybeSync(arg, cb) { + * if (arg) { + * cb(); + * return; + * } + * + * fs.stat('file', cb); + * } + * ``` + * + * This API is hazardous because in the following case: + * + * ```js + * const maybeTrue = Math.random() > 0.5; + * + * maybeSync(maybeTrue, () => { + * foo(); + * }); + * + * bar(); + * ``` + * + * It is not clear whether `foo()` or `bar()` will be called first. + * + * The following approach is much better: + * + * ```js + * import { nextTick } from 'node:process'; + * + * function definitelyAsync(arg, cb) { + * if (arg) { + * nextTick(cb); + * return; + * } + * + * fs.stat('file', cb); + * } + * ``` + * @since v0.1.26 + * @param args Additional arguments to pass when invoking the `callback` + */ + nextTick(callback: Function, ...args: any[]): void; + /** + * The process.noDeprecation property indicates whether the --no-deprecation flag is set on the current Node.js process. + * See the documentation for the ['warning' event](https://nodejs.org/docs/latest/api/process.html#event-warning) and the [emitWarning()](https://nodejs.org/docs/latest/api/process.html#processemitwarningwarning-type-code-ctor) method for more information about this flag's behavior. + */ + noDeprecation?: boolean; + /** + * This API is available through the [--permission](https://nodejs.org/api/cli.html#--permission) flag. + * + * `process.permission` is an object whose methods are used to manage permissions for the current process. + * Additional documentation is available in the [Permission Model](https://nodejs.org/api/permissions.html#permission-model). + * @since v20.0.0 + */ + permission: ProcessPermission; + /** + * The `process.release` property returns an `Object` containing metadata related + * to the current release, including URLs for the source tarball and headers-only + * tarball. + * + * `process.release` contains the following properties: + * + * ```js + * { + * name: 'node', + * lts: 'Hydrogen', + * sourceUrl: 'https://nodejs.org/download/release/v18.12.0/node-v18.12.0.tar.gz', + * headersUrl: 'https://nodejs.org/download/release/v18.12.0/node-v18.12.0-headers.tar.gz', + * libUrl: 'https://nodejs.org/download/release/v18.12.0/win-x64/node.lib' + * } + * ``` + * + * In custom builds from non-release versions of the source tree, only the `name` property may be present. The additional properties should not be + * relied upon to exist. + * @since v3.0.0 + */ + readonly release: ProcessRelease; + readonly features: ProcessFeatures; + /** + * `process.umask()` returns the Node.js process's file mode creation mask. Child + * processes inherit the mask from the parent process. + * @since v0.1.19 + * @deprecated Calling `process.umask()` with no argument causes the process-wide umask to be written twice. This introduces a race condition between threads, and is a potential + * security vulnerability. There is no safe, cross-platform alternative API. + */ + umask(): number; + /** + * Can only be set if not in worker thread. + */ + umask(mask: string | number): number; + /** + * The `process.uptime()` method returns the number of seconds the current Node.js + * process has been running. + * + * The return value includes fractions of a second. Use `Math.floor()` to get whole + * seconds. + * @since v0.5.0 + */ + uptime(): number; + hrtime: HRTime; + /** + * If the Node.js process was spawned with an IPC channel, the process.channel property is a reference to the IPC channel. + * If no IPC channel exists, this property is undefined. + * @since v7.1.0 + */ + channel?: Control; + /** + * If Node.js is spawned with an IPC channel, the `process.send()` method can be + * used to send messages to the parent process. Messages will be received as a `'message'` event on the parent's `ChildProcess` object. + * + * If Node.js was not spawned with an IPC channel, `process.send` will be `undefined`. + * + * The message goes through serialization and parsing. The resulting message might + * not be the same as what is originally sent. + * @since v0.5.9 + * @param options used to parameterize the sending of certain types of handles. `options` supports the following properties: + */ + send?( + message: any, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + send?( + message: any, + sendHandle: SendHandle, + callback?: (error: Error | null) => void, + ): boolean; + send?( + message: any, + callback: (error: Error | null) => void, + ): boolean; + /** + * If the Node.js process is spawned with an IPC channel (see the `Child Process` and `Cluster` documentation), the `process.disconnect()` method will close the + * IPC channel to the parent process, allowing the child process to exit gracefully + * once there are no other connections keeping it alive. + * + * The effect of calling `process.disconnect()` is the same as calling `ChildProcess.disconnect()` from the parent process. + * + * If the Node.js process was not spawned with an IPC channel, `process.disconnect()` will be `undefined`. + * @since v0.7.2 + */ + disconnect(): void; + /** + * If the Node.js process is spawned with an IPC channel (see the `Child Process` and `Cluster` documentation), the `process.connected` property will return `true` so long as the IPC + * channel is connected and will return `false` after `process.disconnect()` is called. + * + * Once `process.connected` is `false`, it is no longer possible to send messages + * over the IPC channel using `process.send()`. + * @since v0.7.2 + */ + connected: boolean; + /** + * The `process.allowedNodeEnvironmentFlags` property is a special, + * read-only `Set` of flags allowable within the `NODE_OPTIONS` environment variable. + * + * `process.allowedNodeEnvironmentFlags` extends `Set`, but overrides `Set.prototype.has` to recognize several different possible flag + * representations. `process.allowedNodeEnvironmentFlags.has()` will + * return `true` in the following cases: + * + * * Flags may omit leading single (`-`) or double (`--`) dashes; e.g., `inspect-brk` for `--inspect-brk`, or `r` for `-r`. + * * Flags passed through to V8 (as listed in `--v8-options`) may replace + * one or more _non-leading_ dashes for an underscore, or vice-versa; + * e.g., `--perf_basic_prof`, `--perf-basic-prof`, `--perf_basic-prof`, + * etc. + * * Flags may contain one or more equals (`=`) characters; all + * characters after and including the first equals will be ignored; + * e.g., `--stack-trace-limit=100`. + * * Flags _must_ be allowable within `NODE_OPTIONS`. + * + * When iterating over `process.allowedNodeEnvironmentFlags`, flags will + * appear only _once_; each will begin with one or more dashes. Flags + * passed through to V8 will contain underscores instead of non-leading + * dashes: + * + * ```js + * import { allowedNodeEnvironmentFlags } from 'node:process'; + * + * allowedNodeEnvironmentFlags.forEach((flag) => { + * // -r + * // --inspect-brk + * // --abort_on_uncaught_exception + * // ... + * }); + * ``` + * + * The methods `add()`, `clear()`, and `delete()` of`process.allowedNodeEnvironmentFlags` do nothing, and will fail + * silently. + * + * If Node.js was compiled _without_ `NODE_OPTIONS` support (shown in {@link config}), `process.allowedNodeEnvironmentFlags` will + * contain what _would have_ been allowable. + * @since v10.10.0 + */ + allowedNodeEnvironmentFlags: ReadonlySet; + /** + * `process.report` is an object whose methods are used to generate diagnostic reports for the current process. + * Additional documentation is available in the [report documentation](https://nodejs.org/docs/latest-v22.x/api/report.html). + * @since v11.8.0 + */ + report: ProcessReport; + /** + * ```js + * import { resourceUsage } from 'node:process'; + * + * console.log(resourceUsage()); + * /* + * Will output: + * { + * userCPUTime: 82872, + * systemCPUTime: 4143, + * maxRSS: 33164, + * sharedMemorySize: 0, + * unsharedDataSize: 0, + * unsharedStackSize: 0, + * minorPageFault: 2469, + * majorPageFault: 0, + * swappedOut: 0, + * fsRead: 0, + * fsWrite: 8, + * ipcSent: 0, + * ipcReceived: 0, + * signalsCount: 0, + * voluntaryContextSwitches: 79, + * involuntaryContextSwitches: 1 + * } + * + * ``` + * @since v12.6.0 + * @return the resource usage for the current process. All of these values come from the `uv_getrusage` call which returns a [`uv_rusage_t` struct][uv_rusage_t]. + */ + resourceUsage(): ResourceUsage; + /** + * The initial value of `process.throwDeprecation` indicates whether the `--throw-deprecation` flag is set on the current Node.js process. `process.throwDeprecation` + * is mutable, so whether or not deprecation warnings result in errors may be altered at runtime. See the documentation for the 'warning' event and the emitWarning() + * method for more information. + * + * ```bash + * $ node --throw-deprecation -p "process.throwDeprecation" + * true + * $ node -p "process.throwDeprecation" + * undefined + * $ node + * > process.emitWarning('test', 'DeprecationWarning'); + * undefined + * > (node:26598) DeprecationWarning: test + * > process.throwDeprecation = true; + * true + * > process.emitWarning('test', 'DeprecationWarning'); + * Thrown: + * [DeprecationWarning: test] { name: 'DeprecationWarning' } + * ``` + * @since v0.9.12 + */ + throwDeprecation: boolean; + /** + * The `process.traceDeprecation` property indicates whether the `--trace-deprecation` flag is set on the current Node.js process. See the + * documentation for the `'warning' event` and the `emitWarning() method` for more information about this + * flag's behavior. + * @since v0.8.0 + */ + traceDeprecation: boolean; + /** + * An object is "refable" if it implements the Node.js "Refable protocol". + * Specifically, this means that the object implements the `Symbol.for('nodejs.ref')` + * and `Symbol.for('nodejs.unref')` methods. "Ref'd" objects will keep the Node.js + * event loop alive, while "unref'd" objects will not. Historically, this was + * implemented by using `ref()` and `unref()` methods directly on the objects. + * This pattern, however, is being deprecated in favor of the "Refable protocol" + * in order to better support Web Platform API types whose APIs cannot be modified + * to add `ref()` and `unref()` methods but still need to support that behavior. + * @since v22.14.0 + * @experimental + * @param maybeRefable An object that may be "refable". + */ + ref(maybeRefable: any): void; + /** + * An object is "unrefable" if it implements the Node.js "Refable protocol". + * Specifically, this means that the object implements the `Symbol.for('nodejs.ref')` + * and `Symbol.for('nodejs.unref')` methods. "Ref'd" objects will keep the Node.js + * event loop alive, while "unref'd" objects will not. Historically, this was + * implemented by using `ref()` and `unref()` methods directly on the objects. + * This pattern, however, is being deprecated in favor of the "Refable protocol" + * in order to better support Web Platform API types whose APIs cannot be modified + * to add `ref()` and `unref()` methods but still need to support that behavior. + * @since v22.14.0 + * @experimental + * @param maybeRefable An object that may be "unref'd". + */ + unref(maybeRefable: any): void; + /** + * Replaces the current process with a new process. + * + * This is achieved by using the `execve` POSIX function and therefore no memory or other + * resources from the current process are preserved, except for the standard input, + * standard output and standard error file descriptor. + * + * All other resources are discarded by the system when the processes are swapped, without triggering + * any exit or close events and without running any cleanup handler. + * + * This function will never return, unless an error occurred. + * + * This function is not available on Windows or IBM i. + * @since v22.15.0 + * @experimental + * @param file The name or path of the executable file to run. + * @param args List of string arguments. No argument can contain a null-byte (`\u0000`). + * @param env Environment key-value pairs. + * No key or value can contain a null-byte (`\u0000`). + * **Default:** `process.env`. + */ + execve?(file: string, args?: readonly string[], env?: ProcessEnv): never; + /* EventEmitter */ + addListener(event: "beforeExit", listener: BeforeExitListener): this; + addListener(event: "disconnect", listener: DisconnectListener): this; + addListener(event: "exit", listener: ExitListener): this; + addListener(event: "rejectionHandled", listener: RejectionHandledListener): this; + addListener(event: "uncaughtException", listener: UncaughtExceptionListener): this; + addListener(event: "uncaughtExceptionMonitor", listener: UncaughtExceptionListener): this; + addListener(event: "unhandledRejection", listener: UnhandledRejectionListener): this; + addListener(event: "warning", listener: WarningListener): this; + addListener(event: "message", listener: MessageListener): this; + addListener(event: "workerMessage", listener: (value: any, source: number) => void): this; + addListener(event: Signals, listener: SignalsListener): this; + addListener(event: "multipleResolves", listener: MultipleResolveListener): this; + addListener(event: "worker", listener: WorkerListener): this; + emit(event: "beforeExit", code: number): boolean; + emit(event: "disconnect"): boolean; + emit(event: "exit", code: number): boolean; + emit(event: "rejectionHandled", promise: Promise): boolean; + emit(event: "uncaughtException", error: Error): boolean; + emit(event: "uncaughtExceptionMonitor", error: Error): boolean; + emit(event: "unhandledRejection", reason: unknown, promise: Promise): boolean; + emit(event: "warning", warning: Error): boolean; + emit(event: "message", message: unknown, sendHandle: SendHandle): this; + emit(event: "workerMessage", value: any, source: number): this; + emit(event: Signals, signal?: Signals): boolean; + emit( + event: "multipleResolves", + type: MultipleResolveType, + promise: Promise, + value: unknown, + ): this; + emit(event: "worker", listener: WorkerListener): this; + on(event: "beforeExit", listener: BeforeExitListener): this; + on(event: "disconnect", listener: DisconnectListener): this; + on(event: "exit", listener: ExitListener): this; + on(event: "rejectionHandled", listener: RejectionHandledListener): this; + on(event: "uncaughtException", listener: UncaughtExceptionListener): this; + on(event: "uncaughtExceptionMonitor", listener: UncaughtExceptionListener): this; + on(event: "unhandledRejection", listener: UnhandledRejectionListener): this; + on(event: "warning", listener: WarningListener): this; + on(event: "message", listener: MessageListener): this; + on(event: Signals, listener: SignalsListener): this; + on(event: "multipleResolves", listener: MultipleResolveListener): this; + on(event: "worker", listener: WorkerListener): this; + on(event: "workerMessage", listener: (value: any, source: number) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "beforeExit", listener: BeforeExitListener): this; + once(event: "disconnect", listener: DisconnectListener): this; + once(event: "exit", listener: ExitListener): this; + once(event: "rejectionHandled", listener: RejectionHandledListener): this; + once(event: "uncaughtException", listener: UncaughtExceptionListener): this; + once(event: "uncaughtExceptionMonitor", listener: UncaughtExceptionListener): this; + once(event: "unhandledRejection", listener: UnhandledRejectionListener): this; + once(event: "warning", listener: WarningListener): this; + once(event: "message", listener: MessageListener): this; + once(event: Signals, listener: SignalsListener): this; + once(event: "multipleResolves", listener: MultipleResolveListener): this; + once(event: "worker", listener: WorkerListener): this; + once(event: "workerMessage", listener: (value: any, source: number) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "beforeExit", listener: BeforeExitListener): this; + prependListener(event: "disconnect", listener: DisconnectListener): this; + prependListener(event: "exit", listener: ExitListener): this; + prependListener(event: "rejectionHandled", listener: RejectionHandledListener): this; + prependListener(event: "uncaughtException", listener: UncaughtExceptionListener): this; + prependListener(event: "uncaughtExceptionMonitor", listener: UncaughtExceptionListener): this; + prependListener(event: "unhandledRejection", listener: UnhandledRejectionListener): this; + prependListener(event: "warning", listener: WarningListener): this; + prependListener(event: "message", listener: MessageListener): this; + prependListener(event: "workerMessage", listener: (value: any, source: number) => void): this; + prependListener(event: Signals, listener: SignalsListener): this; + prependListener(event: "multipleResolves", listener: MultipleResolveListener): this; + prependListener(event: "worker", listener: WorkerListener): this; + prependOnceListener(event: "beforeExit", listener: BeforeExitListener): this; + prependOnceListener(event: "disconnect", listener: DisconnectListener): this; + prependOnceListener(event: "exit", listener: ExitListener): this; + prependOnceListener(event: "rejectionHandled", listener: RejectionHandledListener): this; + prependOnceListener(event: "uncaughtException", listener: UncaughtExceptionListener): this; + prependOnceListener(event: "uncaughtExceptionMonitor", listener: UncaughtExceptionListener): this; + prependOnceListener(event: "unhandledRejection", listener: UnhandledRejectionListener): this; + prependOnceListener(event: "warning", listener: WarningListener): this; + prependOnceListener(event: "message", listener: MessageListener): this; + prependOnceListener(event: "workerMessage", listener: (value: any, source: number) => void): this; + prependOnceListener(event: Signals, listener: SignalsListener): this; + prependOnceListener(event: "multipleResolves", listener: MultipleResolveListener): this; + prependOnceListener(event: "worker", listener: WorkerListener): this; + listeners(event: "beforeExit"): BeforeExitListener[]; + listeners(event: "disconnect"): DisconnectListener[]; + listeners(event: "exit"): ExitListener[]; + listeners(event: "rejectionHandled"): RejectionHandledListener[]; + listeners(event: "uncaughtException"): UncaughtExceptionListener[]; + listeners(event: "uncaughtExceptionMonitor"): UncaughtExceptionListener[]; + listeners(event: "unhandledRejection"): UnhandledRejectionListener[]; + listeners(event: "warning"): WarningListener[]; + listeners(event: "message"): MessageListener[]; + listeners(event: "workerMessage"): ((value: any, source: number) => void)[]; + listeners(event: Signals): SignalsListener[]; + listeners(event: "multipleResolves"): MultipleResolveListener[]; + listeners(event: "worker"): WorkerListener[]; + } + } + } + export = process; +} +declare module "node:process" { + import process = require("process"); + export = process; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/punycode.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/punycode.d.ts new file mode 100644 index 00000000..655c47b6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/punycode.d.ts @@ -0,0 +1,117 @@ +/** + * **The version of the punycode module bundled in Node.js is being deprecated. **In a future major version of Node.js this module will be removed. Users + * currently depending on the `punycode` module should switch to using the + * userland-provided [Punycode.js](https://github.com/bestiejs/punycode.js) module instead. For punycode-based URL + * encoding, see `url.domainToASCII` or, more generally, the `WHATWG URL API`. + * + * The `punycode` module is a bundled version of the [Punycode.js](https://github.com/bestiejs/punycode.js) module. It + * can be accessed using: + * + * ```js + * import punycode from 'node:punycode'; + * ``` + * + * [Punycode](https://tools.ietf.org/html/rfc3492) is a character encoding scheme defined by RFC 3492 that is + * primarily intended for use in Internationalized Domain Names. Because host + * names in URLs are limited to ASCII characters only, Domain Names that contain + * non-ASCII characters must be converted into ASCII using the Punycode scheme. + * For instance, the Japanese character that translates into the English word, `'example'` is `'例'`. The Internationalized Domain Name, `'例.com'` (equivalent + * to `'example.com'`) is represented by Punycode as the ASCII string `'xn--fsq.com'`. + * + * The `punycode` module provides a simple implementation of the Punycode standard. + * + * The `punycode` module is a third-party dependency used by Node.js and + * made available to developers as a convenience. Fixes or other modifications to + * the module must be directed to the [Punycode.js](https://github.com/bestiejs/punycode.js) project. + * @deprecated Since v7.0.0 - Deprecated + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/punycode.js) + */ +declare module "punycode" { + /** + * The `punycode.decode()` method converts a [Punycode](https://tools.ietf.org/html/rfc3492) string of ASCII-only + * characters to the equivalent string of Unicode codepoints. + * + * ```js + * punycode.decode('maana-pta'); // 'mañana' + * punycode.decode('--dqo34k'); // '☃-⌘' + * ``` + * @since v0.5.1 + */ + function decode(string: string): string; + /** + * The `punycode.encode()` method converts a string of Unicode codepoints to a [Punycode](https://tools.ietf.org/html/rfc3492) string of ASCII-only characters. + * + * ```js + * punycode.encode('mañana'); // 'maana-pta' + * punycode.encode('☃-⌘'); // '--dqo34k' + * ``` + * @since v0.5.1 + */ + function encode(string: string): string; + /** + * The `punycode.toUnicode()` method converts a string representing a domain name + * containing [Punycode](https://tools.ietf.org/html/rfc3492) encoded characters into Unicode. Only the [Punycode](https://tools.ietf.org/html/rfc3492) encoded parts of the domain name are be + * converted. + * + * ```js + * // decode domain names + * punycode.toUnicode('xn--maana-pta.com'); // 'mañana.com' + * punycode.toUnicode('xn----dqo34k.com'); // '☃-⌘.com' + * punycode.toUnicode('example.com'); // 'example.com' + * ``` + * @since v0.6.1 + */ + function toUnicode(domain: string): string; + /** + * The `punycode.toASCII()` method converts a Unicode string representing an + * Internationalized Domain Name to [Punycode](https://tools.ietf.org/html/rfc3492). Only the non-ASCII parts of the + * domain name will be converted. Calling `punycode.toASCII()` on a string that + * already only contains ASCII characters will have no effect. + * + * ```js + * // encode domain names + * punycode.toASCII('mañana.com'); // 'xn--maana-pta.com' + * punycode.toASCII('☃-⌘.com'); // 'xn----dqo34k.com' + * punycode.toASCII('example.com'); // 'example.com' + * ``` + * @since v0.6.1 + */ + function toASCII(domain: string): string; + /** + * @deprecated since v7.0.0 + * The version of the punycode module bundled in Node.js is being deprecated. + * In a future major version of Node.js this module will be removed. + * Users currently depending on the punycode module should switch to using + * the userland-provided Punycode.js module instead. + */ + const ucs2: ucs2; + interface ucs2 { + /** + * @deprecated since v7.0.0 + * The version of the punycode module bundled in Node.js is being deprecated. + * In a future major version of Node.js this module will be removed. + * Users currently depending on the punycode module should switch to using + * the userland-provided Punycode.js module instead. + */ + decode(string: string): number[]; + /** + * @deprecated since v7.0.0 + * The version of the punycode module bundled in Node.js is being deprecated. + * In a future major version of Node.js this module will be removed. + * Users currently depending on the punycode module should switch to using + * the userland-provided Punycode.js module instead. + */ + encode(codePoints: readonly number[]): string; + } + /** + * @deprecated since v7.0.0 + * The version of the punycode module bundled in Node.js is being deprecated. + * In a future major version of Node.js this module will be removed. + * Users currently depending on the punycode module should switch to using + * the userland-provided Punycode.js module instead. + */ + const version: string; +} +declare module "node:punycode" { + export * from "punycode"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/querystring.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/querystring.d.ts new file mode 100644 index 00000000..f0d52570 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/querystring.d.ts @@ -0,0 +1,152 @@ +/** + * The `node:querystring` module provides utilities for parsing and formatting URL + * query strings. It can be accessed using: + * + * ```js + * import querystring from 'node:querystring'; + * ``` + * + * `querystring` is more performant than `URLSearchParams` but is not a + * standardized API. Use `URLSearchParams` when performance is not critical or + * when compatibility with browser code is desirable. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/querystring.js) + */ +declare module "querystring" { + interface StringifyOptions { + /** + * The function to use when converting URL-unsafe characters to percent-encoding in the query string. + * @default `querystring.escape()` + */ + encodeURIComponent?: ((str: string) => string) | undefined; + } + interface ParseOptions { + /** + * Specifies the maximum number of keys to parse. Specify `0` to remove key counting limitations. + * @default 1000 + */ + maxKeys?: number | undefined; + /** + * The function to use when decoding percent-encoded characters in the query string. + * @default `querystring.unescape()` + */ + decodeURIComponent?: ((str: string) => string) | undefined; + } + interface ParsedUrlQuery extends NodeJS.Dict {} + interface ParsedUrlQueryInput extends + NodeJS.Dict< + | string + | number + | boolean + | bigint + | ReadonlyArray + | null + > + {} + /** + * The `querystring.stringify()` method produces a URL query string from a + * given `obj` by iterating through the object's "own properties". + * + * It serializes the following types of values passed in `obj`: [string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | + * [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) | + * [bigint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) | + * [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) | + * [string\[\]](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | + * [number\[\]](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) | + * [bigint\[\]](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) | + * [boolean\[\]](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) The numeric values must be finite. Any other input values will be coerced to + * empty strings. + * + * ```js + * querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' }); + * // Returns 'foo=bar&baz=qux&baz=quux&corge=' + * + * querystring.stringify({ foo: 'bar', baz: 'qux' }, ';', ':'); + * // Returns 'foo:bar;baz:qux' + * ``` + * + * By default, characters requiring percent-encoding within the query string will + * be encoded as UTF-8\. If an alternative encoding is required, then an alternative `encodeURIComponent` option will need to be specified: + * + * ```js + * // Assuming gbkEncodeURIComponent function already exists, + * + * querystring.stringify({ w: '中文', foo: 'bar' }, null, null, + * { encodeURIComponent: gbkEncodeURIComponent }); + * ``` + * @since v0.1.25 + * @param obj The object to serialize into a URL query string + * @param [sep='&'] The substring used to delimit key and value pairs in the query string. + * @param [eq='='] . The substring used to delimit keys and values in the query string. + */ + function stringify(obj?: ParsedUrlQueryInput, sep?: string, eq?: string, options?: StringifyOptions): string; + /** + * The `querystring.parse()` method parses a URL query string (`str`) into a + * collection of key and value pairs. + * + * For example, the query string `'foo=bar&abc=xyz&abc=123'` is parsed into: + * + * ```json + * { + * "foo": "bar", + * "abc": ["xyz", "123"] + * } + * ``` + * + * The object returned by the `querystring.parse()` method _does not_ prototypically inherit from the JavaScript `Object`. This means that typical `Object` methods such as `obj.toString()`, + * `obj.hasOwnProperty()`, and others + * are not defined and _will not work_. + * + * By default, percent-encoded characters within the query string will be assumed + * to use UTF-8 encoding. If an alternative character encoding is used, then an + * alternative `decodeURIComponent` option will need to be specified: + * + * ```js + * // Assuming gbkDecodeURIComponent function already exists... + * + * querystring.parse('w=%D6%D0%CE%C4&foo=bar', null, null, + * { decodeURIComponent: gbkDecodeURIComponent }); + * ``` + * @since v0.1.25 + * @param str The URL query string to parse + * @param [sep='&'] The substring used to delimit key and value pairs in the query string. + * @param [eq='='] The substring used to delimit keys and values in the query string. + */ + function parse(str: string, sep?: string, eq?: string, options?: ParseOptions): ParsedUrlQuery; + /** + * The querystring.encode() function is an alias for querystring.stringify(). + */ + const encode: typeof stringify; + /** + * The querystring.decode() function is an alias for querystring.parse(). + */ + const decode: typeof parse; + /** + * The `querystring.escape()` method performs URL percent-encoding on the given `str` in a manner that is optimized for the specific requirements of URL + * query strings. + * + * The `querystring.escape()` method is used by `querystring.stringify()` and is + * generally not expected to be used directly. It is exported primarily to allow + * application code to provide a replacement percent-encoding implementation if + * necessary by assigning `querystring.escape` to an alternative function. + * @since v0.1.25 + */ + function escape(str: string): string; + /** + * The `querystring.unescape()` method performs decoding of URL percent-encoded + * characters on the given `str`. + * + * The `querystring.unescape()` method is used by `querystring.parse()` and is + * generally not expected to be used directly. It is exported primarily to allow + * application code to provide a replacement decoding implementation if + * necessary by assigning `querystring.unescape` to an alternative function. + * + * By default, the `querystring.unescape()` method will attempt to use the + * JavaScript built-in `decodeURIComponent()` method to decode. If that fails, + * a safer equivalent that does not throw on malformed URLs will be used. + * @since v0.1.25 + */ + function unescape(str: string): string; +} +declare module "node:querystring" { + export * from "querystring"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/readline.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/readline.d.ts new file mode 100644 index 00000000..338972e7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/readline.d.ts @@ -0,0 +1,594 @@ +/** + * The `node:readline` module provides an interface for reading data from a [Readable](https://nodejs.org/docs/latest-v22.x/api/stream.html#readable-streams) stream + * (such as [`process.stdin`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdin)) one line at a time. + * + * To use the promise-based APIs: + * + * ```js + * import * as readline from 'node:readline/promises'; + * ``` + * + * To use the callback and sync APIs: + * + * ```js + * import * as readline from 'node:readline'; + * ``` + * + * The following simple example illustrates the basic use of the `node:readline` module. + * + * ```js + * import * as readline from 'node:readline/promises'; + * import { stdin as input, stdout as output } from 'node:process'; + * + * const rl = readline.createInterface({ input, output }); + * + * const answer = await rl.question('What do you think of Node.js? '); + * + * console.log(`Thank you for your valuable feedback: ${answer}`); + * + * rl.close(); + * ``` + * + * Once this code is invoked, the Node.js application will not terminate until the `readline.Interface` is closed because the interface waits for data to be + * received on the `input` stream. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/readline.js) + */ +declare module "readline" { + import { Abortable, EventEmitter } from "node:events"; + import * as promises from "node:readline/promises"; + export { promises }; + export interface Key { + sequence?: string | undefined; + name?: string | undefined; + ctrl?: boolean | undefined; + meta?: boolean | undefined; + shift?: boolean | undefined; + } + /** + * Instances of the `readline.Interface` class are constructed using the `readline.createInterface()` method. Every instance is associated with a + * single `input` [Readable](https://nodejs.org/docs/latest-v22.x/api/stream.html#readable-streams) stream and a single `output` [Writable](https://nodejs.org/docs/latest-v22.x/api/stream.html#writable-streams) stream. + * The `output` stream is used to print prompts for user input that arrives on, + * and is read from, the `input` stream. + * @since v0.1.104 + */ + export class Interface extends EventEmitter implements Disposable { + readonly terminal: boolean; + /** + * The current input data being processed by node. + * + * This can be used when collecting input from a TTY stream to retrieve the + * current value that has been processed thus far, prior to the `line` event + * being emitted. Once the `line` event has been emitted, this property will + * be an empty string. + * + * Be aware that modifying the value during the instance runtime may have + * unintended consequences if `rl.cursor` is not also controlled. + * + * **If not using a TTY stream for input, use the `'line'` event.** + * + * One possible use case would be as follows: + * + * ```js + * const values = ['lorem ipsum', 'dolor sit amet']; + * const rl = readline.createInterface(process.stdin); + * const showResults = debounce(() => { + * console.log( + * '\n', + * values.filter((val) => val.startsWith(rl.line)).join(' '), + * ); + * }, 300); + * process.stdin.on('keypress', (c, k) => { + * showResults(); + * }); + * ``` + * @since v0.1.98 + */ + readonly line: string; + /** + * The cursor position relative to `rl.line`. + * + * This will track where the current cursor lands in the input string, when + * reading input from a TTY stream. The position of cursor determines the + * portion of the input string that will be modified as input is processed, + * as well as the column where the terminal caret will be rendered. + * @since v0.1.98 + */ + readonly cursor: number; + /** + * NOTE: According to the documentation: + * + * > Instances of the `readline.Interface` class are constructed using the + * > `readline.createInterface()` method. + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/readline.html#class-interfaceconstructor + */ + protected constructor( + input: NodeJS.ReadableStream, + output?: NodeJS.WritableStream, + completer?: Completer | AsyncCompleter, + terminal?: boolean, + ); + /** + * NOTE: According to the documentation: + * + * > Instances of the `readline.Interface` class are constructed using the + * > `readline.createInterface()` method. + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/readline.html#class-interfaceconstructor + */ + protected constructor(options: ReadLineOptions); + /** + * The `rl.getPrompt()` method returns the current prompt used by `rl.prompt()`. + * @since v15.3.0, v14.17.0 + * @return the current prompt string + */ + getPrompt(): string; + /** + * The `rl.setPrompt()` method sets the prompt that will be written to `output` whenever `rl.prompt()` is called. + * @since v0.1.98 + */ + setPrompt(prompt: string): void; + /** + * The `rl.prompt()` method writes the `Interface` instances configured`prompt` to a new line in `output` in order to provide a user with a new + * location at which to provide input. + * + * When called, `rl.prompt()` will resume the `input` stream if it has been + * paused. + * + * If the `Interface` was created with `output` set to `null` or `undefined` the prompt is not written. + * @since v0.1.98 + * @param preserveCursor If `true`, prevents the cursor placement from being reset to `0`. + */ + prompt(preserveCursor?: boolean): void; + /** + * The `rl.question()` method displays the `query` by writing it to the `output`, + * waits for user input to be provided on `input`, then invokes the `callback` function passing the provided input as the first argument. + * + * When called, `rl.question()` will resume the `input` stream if it has been + * paused. + * + * If the `Interface` was created with `output` set to `null` or `undefined` the `query` is not written. + * + * The `callback` function passed to `rl.question()` does not follow the typical + * pattern of accepting an `Error` object or `null` as the first argument. + * The `callback` is called with the provided answer as the only argument. + * + * An error will be thrown if calling `rl.question()` after `rl.close()`. + * + * Example usage: + * + * ```js + * rl.question('What is your favorite food? ', (answer) => { + * console.log(`Oh, so your favorite food is ${answer}`); + * }); + * ``` + * + * Using an `AbortController` to cancel a question. + * + * ```js + * const ac = new AbortController(); + * const signal = ac.signal; + * + * rl.question('What is your favorite food? ', { signal }, (answer) => { + * console.log(`Oh, so your favorite food is ${answer}`); + * }); + * + * signal.addEventListener('abort', () => { + * console.log('The food question timed out'); + * }, { once: true }); + * + * setTimeout(() => ac.abort(), 10000); + * ``` + * @since v0.3.3 + * @param query A statement or query to write to `output`, prepended to the prompt. + * @param callback A callback function that is invoked with the user's input in response to the `query`. + */ + question(query: string, callback: (answer: string) => void): void; + question(query: string, options: Abortable, callback: (answer: string) => void): void; + /** + * The `rl.pause()` method pauses the `input` stream, allowing it to be resumed + * later if necessary. + * + * Calling `rl.pause()` does not immediately pause other events (including `'line'`) from being emitted by the `Interface` instance. + * @since v0.3.4 + */ + pause(): this; + /** + * The `rl.resume()` method resumes the `input` stream if it has been paused. + * @since v0.3.4 + */ + resume(): this; + /** + * The `rl.close()` method closes the `Interface` instance and + * relinquishes control over the `input` and `output` streams. When called, + * the `'close'` event will be emitted. + * + * Calling `rl.close()` does not immediately stop other events (including `'line'`) + * from being emitted by the `Interface` instance. + * @since v0.1.98 + */ + close(): void; + /** + * Alias for `rl.close()`. + * @since v22.15.0 + */ + [Symbol.dispose](): void; + /** + * The `rl.write()` method will write either `data` or a key sequence identified + * by `key` to the `output`. The `key` argument is supported only if `output` is + * a `TTY` text terminal. See `TTY keybindings` for a list of key + * combinations. + * + * If `key` is specified, `data` is ignored. + * + * When called, `rl.write()` will resume the `input` stream if it has been + * paused. + * + * If the `Interface` was created with `output` set to `null` or `undefined` the `data` and `key` are not written. + * + * ```js + * rl.write('Delete this!'); + * // Simulate Ctrl+U to delete the line written previously + * rl.write(null, { ctrl: true, name: 'u' }); + * ``` + * + * The `rl.write()` method will write the data to the `readline` `Interface`'s `input` _as if it were provided by the user_. + * @since v0.1.98 + */ + write(data: string | Buffer, key?: Key): void; + write(data: undefined | null | string | Buffer, key: Key): void; + /** + * Returns the real position of the cursor in relation to the input + * prompt + string. Long input (wrapping) strings, as well as multiple + * line prompts are included in the calculations. + * @since v13.5.0, v12.16.0 + */ + getCursorPos(): CursorPos; + /** + * events.EventEmitter + * 1. close + * 2. line + * 3. pause + * 4. resume + * 5. SIGCONT + * 6. SIGINT + * 7. SIGTSTP + * 8. history + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "line", listener: (input: string) => void): this; + addListener(event: "pause", listener: () => void): this; + addListener(event: "resume", listener: () => void): this; + addListener(event: "SIGCONT", listener: () => void): this; + addListener(event: "SIGINT", listener: () => void): this; + addListener(event: "SIGTSTP", listener: () => void): this; + addListener(event: "history", listener: (history: string[]) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "close"): boolean; + emit(event: "line", input: string): boolean; + emit(event: "pause"): boolean; + emit(event: "resume"): boolean; + emit(event: "SIGCONT"): boolean; + emit(event: "SIGINT"): boolean; + emit(event: "SIGTSTP"): boolean; + emit(event: "history", history: string[]): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "close", listener: () => void): this; + on(event: "line", listener: (input: string) => void): this; + on(event: "pause", listener: () => void): this; + on(event: "resume", listener: () => void): this; + on(event: "SIGCONT", listener: () => void): this; + on(event: "SIGINT", listener: () => void): this; + on(event: "SIGTSTP", listener: () => void): this; + on(event: "history", listener: (history: string[]) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "line", listener: (input: string) => void): this; + once(event: "pause", listener: () => void): this; + once(event: "resume", listener: () => void): this; + once(event: "SIGCONT", listener: () => void): this; + once(event: "SIGINT", listener: () => void): this; + once(event: "SIGTSTP", listener: () => void): this; + once(event: "history", listener: (history: string[]) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "line", listener: (input: string) => void): this; + prependListener(event: "pause", listener: () => void): this; + prependListener(event: "resume", listener: () => void): this; + prependListener(event: "SIGCONT", listener: () => void): this; + prependListener(event: "SIGINT", listener: () => void): this; + prependListener(event: "SIGTSTP", listener: () => void): this; + prependListener(event: "history", listener: (history: string[]) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "line", listener: (input: string) => void): this; + prependOnceListener(event: "pause", listener: () => void): this; + prependOnceListener(event: "resume", listener: () => void): this; + prependOnceListener(event: "SIGCONT", listener: () => void): this; + prependOnceListener(event: "SIGINT", listener: () => void): this; + prependOnceListener(event: "SIGTSTP", listener: () => void): this; + prependOnceListener(event: "history", listener: (history: string[]) => void): this; + [Symbol.asyncIterator](): NodeJS.AsyncIterator; + } + export type ReadLine = Interface; // type forwarded for backwards compatibility + export type Completer = (line: string) => CompleterResult; + export type AsyncCompleter = ( + line: string, + callback: (err?: null | Error, result?: CompleterResult) => void, + ) => void; + export type CompleterResult = [string[], string]; + export interface ReadLineOptions { + /** + * The [`Readable`](https://nodejs.org/docs/latest-v22.x/api/stream.html#readable-streams) stream to listen to + */ + input: NodeJS.ReadableStream; + /** + * The [`Writable`](https://nodejs.org/docs/latest-v22.x/api/stream.html#writable-streams) stream to write readline data to. + */ + output?: NodeJS.WritableStream | undefined; + /** + * An optional function used for Tab autocompletion. + */ + completer?: Completer | AsyncCompleter | undefined; + /** + * `true` if the `input` and `output` streams should be treated like a TTY, + * and have ANSI/VT100 escape codes written to it. + * Default: checking `isTTY` on the `output` stream upon instantiation. + */ + terminal?: boolean | undefined; + /** + * Initial list of history lines. + * This option makes sense only if `terminal` is set to `true` by the user or by an internal `output` check, + * otherwise the history caching mechanism is not initialized at all. + * @default [] + */ + history?: string[] | undefined; + /** + * Maximum number of history lines retained. + * To disable the history set this value to `0`. + * This option makes sense only if `terminal` is set to `true` by the user or by an internal `output` check, + * otherwise the history caching mechanism is not initialized at all. + * @default 30 + */ + historySize?: number | undefined; + /** + * If `true`, when a new input line added to the history list duplicates an older one, + * this removes the older line from the list. + * @default false + */ + removeHistoryDuplicates?: boolean | undefined; + /** + * The prompt string to use. + * @default "> " + */ + prompt?: string | undefined; + /** + * If the delay between `\r` and `\n` exceeds `crlfDelay` milliseconds, + * both `\r` and `\n` will be treated as separate end-of-line input. + * `crlfDelay` will be coerced to a number no less than `100`. + * It can be set to `Infinity`, in which case + * `\r` followed by `\n` will always be considered a single newline + * (which may be reasonable for [reading files](https://nodejs.org/docs/latest-v22.x/api/readline.html#example-read-file-stream-line-by-line) with `\r\n` line delimiter). + * @default 100 + */ + crlfDelay?: number | undefined; + /** + * The duration `readline` will wait for a character + * (when reading an ambiguous key sequence in milliseconds + * one that can both form a complete key sequence using the input read so far + * and can take additional input to complete a longer key sequence). + * @default 500 + */ + escapeCodeTimeout?: number | undefined; + /** + * The number of spaces a tab is equal to (minimum 1). + * @default 8 + */ + tabSize?: number | undefined; + /** + * Allows closing the interface using an AbortSignal. + * Aborting the signal will internally call `close` on the interface. + */ + signal?: AbortSignal | undefined; + } + /** + * The `readline.createInterface()` method creates a new `readline.Interface` instance. + * + * ```js + * import readline from 'node:readline'; + * const rl = readline.createInterface({ + * input: process.stdin, + * output: process.stdout, + * }); + * ``` + * + * Once the `readline.Interface` instance is created, the most common case is to + * listen for the `'line'` event: + * + * ```js + * rl.on('line', (line) => { + * console.log(`Received: ${line}`); + * }); + * ``` + * + * If `terminal` is `true` for this instance then the `output` stream will get + * the best compatibility if it defines an `output.columns` property and emits + * a `'resize'` event on the `output` if or when the columns ever change + * (`process.stdout` does this automatically when it is a TTY). + * + * When creating a `readline.Interface` using `stdin` as input, the program + * will not terminate until it receives an [EOF character](https://en.wikipedia.org/wiki/End-of-file#EOF_character). To exit without + * waiting for user input, call `process.stdin.unref()`. + * @since v0.1.98 + */ + export function createInterface( + input: NodeJS.ReadableStream, + output?: NodeJS.WritableStream, + completer?: Completer | AsyncCompleter, + terminal?: boolean, + ): Interface; + export function createInterface(options: ReadLineOptions): Interface; + /** + * The `readline.emitKeypressEvents()` method causes the given `Readable` stream to begin emitting `'keypress'` events corresponding to received input. + * + * Optionally, `interface` specifies a `readline.Interface` instance for which + * autocompletion is disabled when copy-pasted input is detected. + * + * If the `stream` is a `TTY`, then it must be in raw mode. + * + * This is automatically called by any readline instance on its `input` if the `input` is a terminal. Closing the `readline` instance does not stop + * the `input` from emitting `'keypress'` events. + * + * ```js + * readline.emitKeypressEvents(process.stdin); + * if (process.stdin.isTTY) + * process.stdin.setRawMode(true); + * ``` + * + * ## Example: Tiny CLI + * + * The following example illustrates the use of `readline.Interface` class to + * implement a small command-line interface: + * + * ```js + * import readline from 'node:readline'; + * const rl = readline.createInterface({ + * input: process.stdin, + * output: process.stdout, + * prompt: 'OHAI> ', + * }); + * + * rl.prompt(); + * + * rl.on('line', (line) => { + * switch (line.trim()) { + * case 'hello': + * console.log('world!'); + * break; + * default: + * console.log(`Say what? I might have heard '${line.trim()}'`); + * break; + * } + * rl.prompt(); + * }).on('close', () => { + * console.log('Have a great day!'); + * process.exit(0); + * }); + * ``` + * + * ## Example: Read file stream line-by-Line + * + * A common use case for `readline` is to consume an input file one line at a + * time. The easiest way to do so is leveraging the `fs.ReadStream` API as + * well as a `for await...of` loop: + * + * ```js + * import fs from 'node:fs'; + * import readline from 'node:readline'; + * + * async function processLineByLine() { + * const fileStream = fs.createReadStream('input.txt'); + * + * const rl = readline.createInterface({ + * input: fileStream, + * crlfDelay: Infinity, + * }); + * // Note: we use the crlfDelay option to recognize all instances of CR LF + * // ('\r\n') in input.txt as a single line break. + * + * for await (const line of rl) { + * // Each line in input.txt will be successively available here as `line`. + * console.log(`Line from file: ${line}`); + * } + * } + * + * processLineByLine(); + * ``` + * + * Alternatively, one could use the `'line'` event: + * + * ```js + * import fs from 'node:fs'; + * import readline from 'node:readline'; + * + * const rl = readline.createInterface({ + * input: fs.createReadStream('sample.txt'), + * crlfDelay: Infinity, + * }); + * + * rl.on('line', (line) => { + * console.log(`Line from file: ${line}`); + * }); + * ``` + * + * Currently, `for await...of` loop can be a bit slower. If `async` / `await` flow and speed are both essential, a mixed approach can be applied: + * + * ```js + * import { once } from 'node:events'; + * import { createReadStream } from 'node:fs'; + * import { createInterface } from 'node:readline'; + * + * (async function processLineByLine() { + * try { + * const rl = createInterface({ + * input: createReadStream('big-file.txt'), + * crlfDelay: Infinity, + * }); + * + * rl.on('line', (line) => { + * // Process the line. + * }); + * + * await once(rl, 'close'); + * + * console.log('File processed.'); + * } catch (err) { + * console.error(err); + * } + * })(); + * ``` + * @since v0.7.7 + */ + export function emitKeypressEvents(stream: NodeJS.ReadableStream, readlineInterface?: Interface): void; + export type Direction = -1 | 0 | 1; + export interface CursorPos { + rows: number; + cols: number; + } + /** + * The `readline.clearLine()` method clears current line of given [TTY](https://nodejs.org/docs/latest-v22.x/api/tty.html) stream + * in a specified direction identified by `dir`. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if `stream` wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + export function clearLine(stream: NodeJS.WritableStream, dir: Direction, callback?: () => void): boolean; + /** + * The `readline.clearScreenDown()` method clears the given [TTY](https://nodejs.org/docs/latest-v22.x/api/tty.html) stream from + * the current position of the cursor down. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if `stream` wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + export function clearScreenDown(stream: NodeJS.WritableStream, callback?: () => void): boolean; + /** + * The `readline.cursorTo()` method moves cursor to the specified position in a + * given [TTY](https://nodejs.org/docs/latest-v22.x/api/tty.html) `stream`. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if `stream` wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + export function cursorTo(stream: NodeJS.WritableStream, x: number, y?: number, callback?: () => void): boolean; + /** + * The `readline.moveCursor()` method moves the cursor _relative_ to its current + * position in a given [TTY](https://nodejs.org/docs/latest-v22.x/api/tty.html) `stream`. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if `stream` wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + export function moveCursor(stream: NodeJS.WritableStream, dx: number, dy: number, callback?: () => void): boolean; +} +declare module "node:readline" { + export * from "readline"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/readline/promises.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/readline/promises.d.ts new file mode 100644 index 00000000..5bc9a0c2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/readline/promises.d.ts @@ -0,0 +1,161 @@ +/** + * @since v17.0.0 + */ +declare module "readline/promises" { + import { Abortable } from "node:events"; + import { + CompleterResult, + Direction, + Interface as _Interface, + ReadLineOptions as _ReadLineOptions, + } from "node:readline"; + /** + * Instances of the `readlinePromises.Interface` class are constructed using the `readlinePromises.createInterface()` method. Every instance is associated with a + * single `input` `Readable` stream and a single `output` `Writable` stream. + * The `output` stream is used to print prompts for user input that arrives on, + * and is read from, the `input` stream. + * @since v17.0.0 + */ + class Interface extends _Interface { + /** + * The `rl.question()` method displays the `query` by writing it to the `output`, + * waits for user input to be provided on `input`, then invokes the `callback` function passing the provided input as the first argument. + * + * When called, `rl.question()` will resume the `input` stream if it has been + * paused. + * + * If the `Interface` was created with `output` set to `null` or `undefined` the `query` is not written. + * + * If the question is called after `rl.close()`, it returns a rejected promise. + * + * Example usage: + * + * ```js + * const answer = await rl.question('What is your favorite food? '); + * console.log(`Oh, so your favorite food is ${answer}`); + * ``` + * + * Using an `AbortSignal` to cancel a question. + * + * ```js + * const signal = AbortSignal.timeout(10_000); + * + * signal.addEventListener('abort', () => { + * console.log('The food question timed out'); + * }, { once: true }); + * + * const answer = await rl.question('What is your favorite food? ', { signal }); + * console.log(`Oh, so your favorite food is ${answer}`); + * ``` + * @since v17.0.0 + * @param query A statement or query to write to `output`, prepended to the prompt. + * @return A promise that is fulfilled with the user's input in response to the `query`. + */ + question(query: string): Promise; + question(query: string, options: Abortable): Promise; + } + /** + * @since v17.0.0 + */ + class Readline { + /** + * @param stream A TTY stream. + */ + constructor( + stream: NodeJS.WritableStream, + options?: { + autoCommit?: boolean | undefined; + }, + ); + /** + * The `rl.clearLine()` method adds to the internal list of pending action an + * action that clears current line of the associated `stream` in a specified + * direction identified by `dir`. + * Call `rl.commit()` to see the effect of this method, unless `autoCommit: true` was passed to the constructor. + * @since v17.0.0 + * @return this + */ + clearLine(dir: Direction): this; + /** + * The `rl.clearScreenDown()` method adds to the internal list of pending action an + * action that clears the associated stream from the current position of the + * cursor down. + * Call `rl.commit()` to see the effect of this method, unless `autoCommit: true` was passed to the constructor. + * @since v17.0.0 + * @return this + */ + clearScreenDown(): this; + /** + * The `rl.commit()` method sends all the pending actions to the associated `stream` and clears the internal list of pending actions. + * @since v17.0.0 + */ + commit(): Promise; + /** + * The `rl.cursorTo()` method adds to the internal list of pending action an action + * that moves cursor to the specified position in the associated `stream`. + * Call `rl.commit()` to see the effect of this method, unless `autoCommit: true` was passed to the constructor. + * @since v17.0.0 + * @return this + */ + cursorTo(x: number, y?: number): this; + /** + * The `rl.moveCursor()` method adds to the internal list of pending action an + * action that moves the cursor _relative_ to its current position in the + * associated `stream`. + * Call `rl.commit()` to see the effect of this method, unless `autoCommit: true` was passed to the constructor. + * @since v17.0.0 + * @return this + */ + moveCursor(dx: number, dy: number): this; + /** + * The `rl.rollback` methods clears the internal list of pending actions without + * sending it to the associated `stream`. + * @since v17.0.0 + * @return this + */ + rollback(): this; + } + type Completer = (line: string) => CompleterResult | Promise; + interface ReadLineOptions extends Omit<_ReadLineOptions, "completer"> { + /** + * An optional function used for Tab autocompletion. + */ + completer?: Completer | undefined; + } + /** + * The `readlinePromises.createInterface()` method creates a new `readlinePromises.Interface` instance. + * + * ```js + * import readlinePromises from 'node:readline/promises'; + * const rl = readlinePromises.createInterface({ + * input: process.stdin, + * output: process.stdout, + * }); + * ``` + * + * Once the `readlinePromises.Interface` instance is created, the most common case + * is to listen for the `'line'` event: + * + * ```js + * rl.on('line', (line) => { + * console.log(`Received: ${line}`); + * }); + * ``` + * + * If `terminal` is `true` for this instance then the `output` stream will get + * the best compatibility if it defines an `output.columns` property and emits + * a `'resize'` event on the `output` if or when the columns ever change + * (`process.stdout` does this automatically when it is a TTY). + * @since v17.0.0 + */ + function createInterface( + input: NodeJS.ReadableStream, + output?: NodeJS.WritableStream, + completer?: Completer, + terminal?: boolean, + ): Interface; + function createInterface(options: ReadLineOptions): Interface; +} +declare module "node:readline/promises" { + export * from "readline/promises"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/repl.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/repl.d.ts new file mode 100644 index 00000000..fb858da1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/repl.d.ts @@ -0,0 +1,428 @@ +/** + * The `node:repl` module provides a Read-Eval-Print-Loop (REPL) implementation + * that is available both as a standalone program or includible in other + * applications. It can be accessed using: + * + * ```js + * import repl from 'node:repl'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/repl.js) + */ +declare module "repl" { + import { AsyncCompleter, Completer, Interface } from "node:readline"; + import { Context } from "node:vm"; + import { InspectOptions } from "node:util"; + interface ReplOptions { + /** + * The input prompt to display. + * @default "> " + */ + prompt?: string | undefined; + /** + * The `Readable` stream from which REPL input will be read. + * @default process.stdin + */ + input?: NodeJS.ReadableStream | undefined; + /** + * The `Writable` stream to which REPL output will be written. + * @default process.stdout + */ + output?: NodeJS.WritableStream | undefined; + /** + * If `true`, specifies that the output should be treated as a TTY terminal, and have + * ANSI/VT100 escape codes written to it. + * Default: checking the value of the `isTTY` property on the output stream upon + * instantiation. + */ + terminal?: boolean | undefined; + /** + * The function to be used when evaluating each given line of input. + * **Default:** an async wrapper for the JavaScript `eval()` function. An `eval` function can + * error with `repl.Recoverable` to indicate the input was incomplete and prompt for + * additional lines. See the [custom evaluation functions](https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#custom-evaluation-functions) + * section for more details. + */ + eval?: REPLEval | undefined; + /** + * Defines if the repl prints output previews or not. + * @default `true` Always `false` in case `terminal` is falsy. + */ + preview?: boolean | undefined; + /** + * If `true`, specifies that the default `writer` function should include ANSI color + * styling to REPL output. If a custom `writer` function is provided then this has no + * effect. + * @default the REPL instance's `terminal` value + */ + useColors?: boolean | undefined; + /** + * If `true`, specifies that the default evaluation function will use the JavaScript + * `global` as the context as opposed to creating a new separate context for the REPL + * instance. The node CLI REPL sets this value to `true`. + * @default false + */ + useGlobal?: boolean | undefined; + /** + * If `true`, specifies that the default writer will not output the return value of a + * command if it evaluates to `undefined`. + * @default false + */ + ignoreUndefined?: boolean | undefined; + /** + * The function to invoke to format the output of each command before writing to `output`. + * @default a wrapper for `util.inspect` + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_customizing_repl_output + */ + writer?: REPLWriter | undefined; + /** + * An optional function used for custom Tab auto completion. + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/readline.html#readline_use_of_the_completer_function + */ + completer?: Completer | AsyncCompleter | undefined; + /** + * A flag that specifies whether the default evaluator executes all JavaScript commands in + * strict mode or default (sloppy) mode. + * Accepted values are: + * - `repl.REPL_MODE_SLOPPY` - evaluates expressions in sloppy mode. + * - `repl.REPL_MODE_STRICT` - evaluates expressions in strict mode. This is equivalent to + * prefacing every repl statement with `'use strict'`. + */ + replMode?: typeof REPL_MODE_SLOPPY | typeof REPL_MODE_STRICT | undefined; + /** + * Stop evaluating the current piece of code when `SIGINT` is received, i.e. `Ctrl+C` is + * pressed. This cannot be used together with a custom `eval` function. + * @default false + */ + breakEvalOnSigint?: boolean | undefined; + } + type REPLEval = ( + this: REPLServer, + evalCmd: string, + context: Context, + file: string, + cb: (err: Error | null, result: any) => void, + ) => void; + type REPLWriter = (this: REPLServer, obj: any) => string; + /** + * This is the default "writer" value, if none is passed in the REPL options, + * and it can be overridden by custom print functions. + */ + const writer: REPLWriter & { + options: InspectOptions; + }; + type REPLCommandAction = (this: REPLServer, text: string) => void; + interface REPLCommand { + /** + * Help text to be displayed when `.help` is entered. + */ + help?: string | undefined; + /** + * The function to execute, optionally accepting a single string argument. + */ + action: REPLCommandAction; + } + /** + * Instances of `repl.REPLServer` are created using the {@link start} method + * or directly using the JavaScript `new` keyword. + * + * ```js + * import repl from 'node:repl'; + * + * const options = { useColors: true }; + * + * const firstInstance = repl.start(options); + * const secondInstance = new repl.REPLServer(options); + * ``` + * @since v0.1.91 + */ + class REPLServer extends Interface { + /** + * The `vm.Context` provided to the `eval` function to be used for JavaScript + * evaluation. + */ + readonly context: Context; + /** + * @deprecated since v14.3.0 - Use `input` instead. + */ + readonly inputStream: NodeJS.ReadableStream; + /** + * @deprecated since v14.3.0 - Use `output` instead. + */ + readonly outputStream: NodeJS.WritableStream; + /** + * The `Readable` stream from which REPL input will be read. + */ + readonly input: NodeJS.ReadableStream; + /** + * The `Writable` stream to which REPL output will be written. + */ + readonly output: NodeJS.WritableStream; + /** + * The commands registered via `replServer.defineCommand()`. + */ + readonly commands: NodeJS.ReadOnlyDict; + /** + * A value indicating whether the REPL is currently in "editor mode". + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_commands_and_special_keys + */ + readonly editorMode: boolean; + /** + * A value indicating whether the `_` variable has been assigned. + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_assignment_of_the_underscore_variable + */ + readonly underscoreAssigned: boolean; + /** + * The last evaluation result from the REPL (assigned to the `_` variable inside of the REPL). + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_assignment_of_the_underscore_variable + */ + readonly last: any; + /** + * A value indicating whether the `_error` variable has been assigned. + * + * @since v9.8.0 + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_assignment_of_the_underscore_variable + */ + readonly underscoreErrAssigned: boolean; + /** + * The last error raised inside the REPL (assigned to the `_error` variable inside of the REPL). + * + * @since v9.8.0 + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_assignment_of_the_underscore_variable + */ + readonly lastError: any; + /** + * Specified in the REPL options, this is the function to be used when evaluating each + * given line of input. If not specified in the REPL options, this is an async wrapper + * for the JavaScript `eval()` function. + */ + readonly eval: REPLEval; + /** + * Specified in the REPL options, this is a value indicating whether the default + * `writer` function should include ANSI color styling to REPL output. + */ + readonly useColors: boolean; + /** + * Specified in the REPL options, this is a value indicating whether the default `eval` + * function will use the JavaScript `global` as the context as opposed to creating a new + * separate context for the REPL instance. + */ + readonly useGlobal: boolean; + /** + * Specified in the REPL options, this is a value indicating whether the default `writer` + * function should output the result of a command if it evaluates to `undefined`. + */ + readonly ignoreUndefined: boolean; + /** + * Specified in the REPL options, this is the function to invoke to format the output of + * each command before writing to `outputStream`. If not specified in the REPL options, + * this will be a wrapper for `util.inspect`. + */ + readonly writer: REPLWriter; + /** + * Specified in the REPL options, this is the function to use for custom Tab auto-completion. + */ + readonly completer: Completer | AsyncCompleter; + /** + * Specified in the REPL options, this is a flag that specifies whether the default `eval` + * function should execute all JavaScript commands in strict mode or default (sloppy) mode. + * Possible values are: + * - `repl.REPL_MODE_SLOPPY` - evaluates expressions in sloppy mode. + * - `repl.REPL_MODE_STRICT` - evaluates expressions in strict mode. This is equivalent to + * prefacing every repl statement with `'use strict'`. + */ + readonly replMode: typeof REPL_MODE_SLOPPY | typeof REPL_MODE_STRICT; + /** + * NOTE: According to the documentation: + * + * > Instances of `repl.REPLServer` are created using the `repl.start()` method and + * > _should not_ be created directly using the JavaScript `new` keyword. + * + * `REPLServer` cannot be subclassed due to implementation specifics in NodeJS. + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_class_replserver + */ + private constructor(); + /** + * The `replServer.defineCommand()` method is used to add new `.`\-prefixed commands + * to the REPL instance. Such commands are invoked by typing a `.` followed by the `keyword`. The `cmd` is either a `Function` or an `Object` with the following + * properties: + * + * The following example shows two new commands added to the REPL instance: + * + * ```js + * import repl from 'node:repl'; + * + * const replServer = repl.start({ prompt: '> ' }); + * replServer.defineCommand('sayhello', { + * help: 'Say hello', + * action(name) { + * this.clearBufferedCommand(); + * console.log(`Hello, ${name}!`); + * this.displayPrompt(); + * }, + * }); + * replServer.defineCommand('saybye', function saybye() { + * console.log('Goodbye!'); + * this.close(); + * }); + * ``` + * + * The new commands can then be used from within the REPL instance: + * + * ```console + * > .sayhello Node.js User + * Hello, Node.js User! + * > .saybye + * Goodbye! + * ``` + * @since v0.3.0 + * @param keyword The command keyword (_without_ a leading `.` character). + * @param cmd The function to invoke when the command is processed. + */ + defineCommand(keyword: string, cmd: REPLCommandAction | REPLCommand): void; + /** + * The `replServer.displayPrompt()` method readies the REPL instance for input + * from the user, printing the configured `prompt` to a new line in the `output` and resuming the `input` to accept new input. + * + * When multi-line input is being entered, an ellipsis is printed rather than the + * 'prompt'. + * + * When `preserveCursor` is `true`, the cursor placement will not be reset to `0`. + * + * The `replServer.displayPrompt` method is primarily intended to be called from + * within the action function for commands registered using the `replServer.defineCommand()` method. + * @since v0.1.91 + */ + displayPrompt(preserveCursor?: boolean): void; + /** + * The `replServer.clearBufferedCommand()` method clears any command that has been + * buffered but not yet executed. This method is primarily intended to be + * called from within the action function for commands registered using the `replServer.defineCommand()` method. + * @since v9.0.0 + */ + clearBufferedCommand(): void; + /** + * Initializes a history log file for the REPL instance. When executing the + * Node.js binary and using the command-line REPL, a history file is initialized + * by default. However, this is not the case when creating a REPL + * programmatically. Use this method to initialize a history log file when working + * with REPL instances programmatically. + * @since v11.10.0 + * @param historyPath the path to the history file + * @param callback called when history writes are ready or upon error + */ + setupHistory(path: string, callback: (err: Error | null, repl: this) => void): void; + /** + * events.EventEmitter + * 1. close - inherited from `readline.Interface` + * 2. line - inherited from `readline.Interface` + * 3. pause - inherited from `readline.Interface` + * 4. resume - inherited from `readline.Interface` + * 5. SIGCONT - inherited from `readline.Interface` + * 6. SIGINT - inherited from `readline.Interface` + * 7. SIGTSTP - inherited from `readline.Interface` + * 8. exit + * 9. reset + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "line", listener: (input: string) => void): this; + addListener(event: "pause", listener: () => void): this; + addListener(event: "resume", listener: () => void): this; + addListener(event: "SIGCONT", listener: () => void): this; + addListener(event: "SIGINT", listener: () => void): this; + addListener(event: "SIGTSTP", listener: () => void): this; + addListener(event: "exit", listener: () => void): this; + addListener(event: "reset", listener: (context: Context) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "close"): boolean; + emit(event: "line", input: string): boolean; + emit(event: "pause"): boolean; + emit(event: "resume"): boolean; + emit(event: "SIGCONT"): boolean; + emit(event: "SIGINT"): boolean; + emit(event: "SIGTSTP"): boolean; + emit(event: "exit"): boolean; + emit(event: "reset", context: Context): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "close", listener: () => void): this; + on(event: "line", listener: (input: string) => void): this; + on(event: "pause", listener: () => void): this; + on(event: "resume", listener: () => void): this; + on(event: "SIGCONT", listener: () => void): this; + on(event: "SIGINT", listener: () => void): this; + on(event: "SIGTSTP", listener: () => void): this; + on(event: "exit", listener: () => void): this; + on(event: "reset", listener: (context: Context) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "line", listener: (input: string) => void): this; + once(event: "pause", listener: () => void): this; + once(event: "resume", listener: () => void): this; + once(event: "SIGCONT", listener: () => void): this; + once(event: "SIGINT", listener: () => void): this; + once(event: "SIGTSTP", listener: () => void): this; + once(event: "exit", listener: () => void): this; + once(event: "reset", listener: (context: Context) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "line", listener: (input: string) => void): this; + prependListener(event: "pause", listener: () => void): this; + prependListener(event: "resume", listener: () => void): this; + prependListener(event: "SIGCONT", listener: () => void): this; + prependListener(event: "SIGINT", listener: () => void): this; + prependListener(event: "SIGTSTP", listener: () => void): this; + prependListener(event: "exit", listener: () => void): this; + prependListener(event: "reset", listener: (context: Context) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "line", listener: (input: string) => void): this; + prependOnceListener(event: "pause", listener: () => void): this; + prependOnceListener(event: "resume", listener: () => void): this; + prependOnceListener(event: "SIGCONT", listener: () => void): this; + prependOnceListener(event: "SIGINT", listener: () => void): this; + prependOnceListener(event: "SIGTSTP", listener: () => void): this; + prependOnceListener(event: "exit", listener: () => void): this; + prependOnceListener(event: "reset", listener: (context: Context) => void): this; + } + /** + * A flag passed in the REPL options. Evaluates expressions in sloppy mode. + */ + const REPL_MODE_SLOPPY: unique symbol; + /** + * A flag passed in the REPL options. Evaluates expressions in strict mode. + * This is equivalent to prefacing every repl statement with `'use strict'`. + */ + const REPL_MODE_STRICT: unique symbol; + /** + * The `repl.start()` method creates and starts a {@link REPLServer} instance. + * + * If `options` is a string, then it specifies the input prompt: + * + * ```js + * import repl from 'node:repl'; + * + * // a Unix style prompt + * repl.start('$ '); + * ``` + * @since v0.1.91 + */ + function start(options?: string | ReplOptions): REPLServer; + /** + * Indicates a recoverable error that a `REPLServer` can use to support multi-line input. + * + * @see https://nodejs.org/dist/latest-v22.x/docs/api/repl.html#repl_recoverable_errors + */ + class Recoverable extends SyntaxError { + err: Error; + constructor(err: Error); + } +} +declare module "node:repl" { + export * from "repl"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/sea.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/sea.d.ts new file mode 100644 index 00000000..30130747 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/sea.d.ts @@ -0,0 +1,153 @@ +/** + * This feature allows the distribution of a Node.js application conveniently to a + * system that does not have Node.js installed. + * + * Node.js supports the creation of [single executable applications](https://github.com/nodejs/single-executable) by allowing + * the injection of a blob prepared by Node.js, which can contain a bundled script, + * into the `node` binary. During start up, the program checks if anything has been + * injected. If the blob is found, it executes the script in the blob. Otherwise + * Node.js operates as it normally does. + * + * The single executable application feature currently only supports running a + * single embedded script using the `CommonJS` module system. + * + * Users can create a single executable application from their bundled script + * with the `node` binary itself and any tool which can inject resources into the + * binary. + * + * Here are the steps for creating a single executable application using one such + * tool, [postject](https://github.com/nodejs/postject): + * + * 1. Create a JavaScript file: + * ```bash + * echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js + * ``` + * 2. Create a configuration file building a blob that can be injected into the + * single executable application (see `Generating single executable preparation blobs` for details): + * ```bash + * echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json + * ``` + * 3. Generate the blob to be injected: + * ```bash + * node --experimental-sea-config sea-config.json + * ``` + * 4. Create a copy of the `node` executable and name it according to your needs: + * * On systems other than Windows: + * ```bash + * cp $(command -v node) hello + * ``` + * * On Windows: + * ```text + * node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')" + * ``` + * The `.exe` extension is necessary. + * 5. Remove the signature of the binary (macOS and Windows only): + * * On macOS: + * ```bash + * codesign --remove-signature hello + * ``` + * * On Windows (optional): + * [signtool](https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool) can be used from the installed [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/). + * If this step is + * skipped, ignore any signature-related warning from postject. + * ```powershell + * signtool remove /s hello.exe + * ``` + * 6. Inject the blob into the copied binary by running `postject` with + * the following options: + * * `hello` / `hello.exe` \- The name of the copy of the `node` executable + * created in step 4. + * * `NODE_SEA_BLOB` \- The name of the resource / note / section in the binary + * where the contents of the blob will be stored. + * * `sea-prep.blob` \- The name of the blob created in step 1. + * * `--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` \- The [fuse](https://www.electronjs.org/docs/latest/tutorial/fuses) used by the Node.js project to detect if a file has been + * injected. + * * `--macho-segment-name NODE_SEA` (only needed on macOS) - The name of the + * segment in the binary where the contents of the blob will be + * stored. + * To summarize, here is the required command for each platform: + * * On Linux: + * ```bash + * npx postject hello NODE_SEA_BLOB sea-prep.blob \ + * --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + * ``` + * * On Windows - PowerShell: + * ```powershell + * npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ` + * --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + * ``` + * * On Windows - Command Prompt: + * ```text + * npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ^ + * --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + * ``` + * * On macOS: + * ```bash + * npx postject hello NODE_SEA_BLOB sea-prep.blob \ + * --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ + * --macho-segment-name NODE_SEA + * ``` + * 7. Sign the binary (macOS and Windows only): + * * On macOS: + * ```bash + * codesign --sign - hello + * ``` + * * On Windows (optional): + * A certificate needs to be present for this to work. However, the unsigned + * binary would still be runnable. + * ```powershell + * signtool sign /fd SHA256 hello.exe + * ``` + * 8. Run the binary: + * * On systems other than Windows + * ```console + * $ ./hello world + * Hello, world! + * ``` + * * On Windows + * ```console + * $ .\hello.exe world + * Hello, world! + * ``` + * @since v19.7.0, v18.16.0 + * @experimental + * @see [source](https://github.com/nodejs/node/blob/v22.x/src/node_sea.cc) + */ +declare module "node:sea" { + type AssetKey = string; + /** + * @since v20.12.0 + * @return Whether this script is running inside a single-executable application. + */ + function isSea(): boolean; + /** + * This method can be used to retrieve the assets configured to be bundled into the + * single-executable application at build time. + * An error is thrown when no matching asset can be found. + * @since v20.12.0 + */ + function getAsset(key: AssetKey): ArrayBuffer; + function getAsset(key: AssetKey, encoding: string): string; + /** + * Similar to `sea.getAsset()`, but returns the result in a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). + * An error is thrown when no matching asset can be found. + * @since v20.12.0 + */ + function getAssetAsBlob(key: AssetKey, options?: { + type: string; + }): Blob; + /** + * This method can be used to retrieve the assets configured to be bundled into the + * single-executable application at build time. + * An error is thrown when no matching asset can be found. + * + * Unlike `sea.getRawAsset()` or `sea.getAssetAsBlob()`, this method does not + * return a copy. Instead, it returns the raw asset bundled inside the executable. + * + * For now, users should avoid writing to the returned array buffer. If the + * injected section is not marked as writable or not aligned properly, + * writes to the returned array buffer is likely to result in a crash. + * @since v20.12.0 + */ + function getRawAsset(key: AssetKey): ArrayBuffer; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/sqlite.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/sqlite.d.ts new file mode 100644 index 00000000..19d826d4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/sqlite.d.ts @@ -0,0 +1,721 @@ +/** + * The `node:sqlite` module facilitates working with SQLite databases. + * To access it: + * + * ```js + * import sqlite from 'node:sqlite'; + * ``` + * + * This module is only available under the `node:` scheme. The following will not + * work: + * + * ```js + * import sqlite from 'sqlite'; + * ``` + * + * The following example shows the basic usage of the `node:sqlite` module to open + * an in-memory database, write data to the database, and then read the data back. + * + * ```js + * import { DatabaseSync } from 'node:sqlite'; + * const database = new DatabaseSync(':memory:'); + * + * // Execute SQL statements from strings. + * database.exec(` + * CREATE TABLE data( + * key INTEGER PRIMARY KEY, + * value TEXT + * ) STRICT + * `); + * // Create a prepared statement to insert data into the database. + * const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); + * // Execute the prepared statement with bound values. + * insert.run(1, 'hello'); + * insert.run(2, 'world'); + * // Create a prepared statement to read data from the database. + * const query = database.prepare('SELECT * FROM data ORDER BY key'); + * // Execute the prepared statement and log the result set. + * console.log(query.all()); + * // Prints: [ { key: 1, value: 'hello' }, { key: 2, value: 'world' } ] + * ``` + * @since v22.5.0 + * @experimental + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/sqlite.js) + */ +declare module "node:sqlite" { + import { PathLike } from "node:fs"; + type SQLInputValue = null | number | bigint | string | NodeJS.ArrayBufferView; + type SQLOutputValue = null | number | bigint | string | NodeJS.NonSharedUint8Array; + interface DatabaseSyncOptions { + /** + * If `true`, the database is opened by the constructor. When + * this value is `false`, the database must be opened via the `open()` method. + * @since v22.5.0 + * @default true + */ + open?: boolean | undefined; + /** + * If `true`, foreign key constraints + * are enabled. This is recommended but can be disabled for compatibility with + * legacy database schemas. The enforcement of foreign key constraints can be + * enabled and disabled after opening the database using + * [`PRAGMA foreign_keys`](https://www.sqlite.org/pragma.html#pragma_foreign_keys). + * @since v22.10.0 + * @default true + */ + enableForeignKeyConstraints?: boolean | undefined; + /** + * If `true`, SQLite will accept + * [double-quoted string literals](https://www.sqlite.org/quirks.html#dblquote). + * This is not recommended but can be + * enabled for compatibility with legacy database schemas. + * @since v22.10.0 + * @default false + */ + enableDoubleQuotedStringLiterals?: boolean | undefined; + /** + * If `true`, the database is opened in read-only mode. + * If the database does not exist, opening it will fail. + * @since v22.12.0 + * @default false + */ + readOnly?: boolean | undefined; + /** + * If `true`, the `loadExtension` SQL function + * and the `loadExtension()` method are enabled. + * You can call `enableLoadExtension(false)` later to disable this feature. + * @since v22.13.0 + * @default false + */ + allowExtension?: boolean | undefined; + /** + * The [busy timeout](https://sqlite.org/c3ref/busy_timeout.html) in milliseconds. This is the maximum amount of + * time that SQLite will wait for a database lock to be released before + * returning an error. + * @since v22.16.0 + * @default 0 + */ + timeout?: number | undefined; + /** + * If `true`, integer fields are read as JavaScript `BigInt` values. If `false`, + * integer fields are read as JavaScript numbers. + * @since v22.18.0 + * @default false + */ + readBigInts?: boolean | undefined; + /** + * If `true`, query results are returned as arrays instead of objects. + * @since v22.18.0 + * @default false + */ + returnArrays?: boolean | undefined; + /** + * If `true`, allows binding named parameters without the prefix + * character (e.g., `foo` instead of `:foo`). + * @since v22.18.0 + * @default true + */ + allowBareNamedParameters?: boolean | undefined; + /** + * If `true`, unknown named parameters are ignored when binding. + * If `false`, an exception is thrown for unknown named parameters. + * @since v22.18.0 + * @default false + */ + allowUnknownNamedParameters?: boolean | undefined; + } + interface CreateSessionOptions { + /** + * A specific table to track changes for. By default, changes to all tables are tracked. + * @since v22.12.0 + */ + table?: string | undefined; + /** + * Name of the database to track. This is useful when multiple databases have been added using + * [`ATTACH DATABASE`](https://www.sqlite.org/lang_attach.html). + * @since v22.12.0 + * @default 'main' + */ + db?: string | undefined; + } + interface ApplyChangesetOptions { + /** + * Skip changes that, when targeted table name is supplied to this function, return a truthy value. + * By default, all changes are attempted. + * @since v22.12.0 + */ + filter?: ((tableName: string) => boolean) | undefined; + /** + * A function that determines how to handle conflicts. The function receives one argument, + * which can be one of the following values: + * + * * `SQLITE_CHANGESET_DATA`: A `DELETE` or `UPDATE` change does not contain the expected "before" values. + * * `SQLITE_CHANGESET_NOTFOUND`: A row matching the primary key of the `DELETE` or `UPDATE` change does not exist. + * * `SQLITE_CHANGESET_CONFLICT`: An `INSERT` change results in a duplicate primary key. + * * `SQLITE_CHANGESET_FOREIGN_KEY`: Applying a change would result in a foreign key violation. + * * `SQLITE_CHANGESET_CONSTRAINT`: Applying a change results in a `UNIQUE`, `CHECK`, or `NOT NULL` constraint + * violation. + * + * The function should return one of the following values: + * + * * `SQLITE_CHANGESET_OMIT`: Omit conflicting changes. + * * `SQLITE_CHANGESET_REPLACE`: Replace existing values with conflicting changes (only valid with + `SQLITE_CHANGESET_DATA` or `SQLITE_CHANGESET_CONFLICT` conflicts). + * * `SQLITE_CHANGESET_ABORT`: Abort on conflict and roll back the database. + * + * When an error is thrown in the conflict handler or when any other value is returned from the handler, + * applying the changeset is aborted and the database is rolled back. + * + * **Default**: A function that returns `SQLITE_CHANGESET_ABORT`. + * @since v22.12.0 + */ + onConflict?: ((conflictType: number) => number) | undefined; + } + interface FunctionOptions { + /** + * If `true`, the [`SQLITE_DETERMINISTIC`](https://www.sqlite.org/c3ref/c_deterministic.html) flag is + * set on the created function. + * @default false + */ + deterministic?: boolean | undefined; + /** + * If `true`, the [`SQLITE_DIRECTONLY`](https://www.sqlite.org/c3ref/c_directonly.html) flag is set on + * the created function. + * @default false + */ + directOnly?: boolean | undefined; + /** + * If `true`, integer arguments to `function` + * are converted to `BigInt`s. If `false`, integer arguments are passed as + * JavaScript numbers. + * @default false + */ + useBigIntArguments?: boolean | undefined; + /** + * If `true`, `function` may be invoked with any number of + * arguments (between zero and + * [`SQLITE_MAX_FUNCTION_ARG`](https://www.sqlite.org/limits.html#max_function_arg)). If `false`, + * `function` must be invoked with exactly `function.length` arguments. + * @default false + */ + varargs?: boolean | undefined; + } + interface AggregateOptions extends FunctionOptions { + /** + * The identity value for the aggregation function. This value is used when the aggregation + * function is initialized. When a `Function` is passed the identity will be its return value. + */ + start: T | (() => T); + /** + * The function to call for each row in the aggregation. The + * function receives the current state and the row value. The return value of + * this function should be the new state. + */ + step: (accumulator: T, ...args: SQLOutputValue[]) => T; + /** + * The function to call to get the result of the + * aggregation. The function receives the final state and should return the + * result of the aggregation. + */ + result?: ((accumulator: T) => SQLInputValue) | undefined; + /** + * When this function is provided, the `aggregate` method will work as a window function. + * The function receives the current state and the dropped row value. The return value of this function should be the + * new state. + */ + inverse?: ((accumulator: T, ...args: SQLOutputValue[]) => T) | undefined; + } + /** + * This class represents a single [connection](https://www.sqlite.org/c3ref/sqlite3.html) to a SQLite database. All APIs + * exposed by this class execute synchronously. + * @since v22.5.0 + */ + class DatabaseSync implements Disposable { + /** + * Constructs a new `DatabaseSync` instance. + * @param path The path of the database. + * A SQLite database can be stored in a file or completely [in memory](https://www.sqlite.org/inmemorydb.html). + * To use a file-backed database, the path should be a file path. + * To use an in-memory database, the path should be the special name `':memory:'`. + * @param options Configuration options for the database connection. + */ + constructor(path: PathLike, options?: DatabaseSyncOptions); + /** + * Registers a new aggregate function with the SQLite database. This method is a wrapper around + * [`sqlite3_create_window_function()`](https://www.sqlite.org/c3ref/create_function.html). + * + * When used as a window function, the `result` function will be called multiple times. + * + * ```js + * import { DatabaseSync } from 'node:sqlite'; + * + * const db = new DatabaseSync(':memory:'); + * db.exec(` + * CREATE TABLE t3(x, y); + * INSERT INTO t3 VALUES ('a', 4), + * ('b', 5), + * ('c', 3), + * ('d', 8), + * ('e', 1); + * `); + * + * db.aggregate('sumint', { + * start: 0, + * step: (acc, value) => acc + value, + * }); + * + * db.prepare('SELECT sumint(y) as total FROM t3').get(); // { total: 21 } + * ``` + * @since v22.16.0 + * @param name The name of the SQLite function to create. + * @param options Function configuration settings. + */ + aggregate(name: string, options: AggregateOptions): void; + aggregate(name: string, options: AggregateOptions): void; + /** + * Closes the database connection. An exception is thrown if the database is not + * open. This method is a wrapper around [`sqlite3_close_v2()`](https://www.sqlite.org/c3ref/close.html). + * @since v22.5.0 + */ + close(): void; + /** + * Loads a shared library into the database connection. This method is a wrapper + * around [`sqlite3_load_extension()`](https://www.sqlite.org/c3ref/load_extension.html). It is required to enable the + * `allowExtension` option when constructing the `DatabaseSync` instance. + * @since v22.13.0 + * @param path The path to the shared library to load. + */ + loadExtension(path: string): void; + /** + * Enables or disables the `loadExtension` SQL function, and the `loadExtension()` + * method. When `allowExtension` is `false` when constructing, you cannot enable + * loading extensions for security reasons. + * @since v22.13.0 + * @param allow Whether to allow loading extensions. + */ + enableLoadExtension(allow: boolean): void; + /** + * This method is a wrapper around [`sqlite3_db_filename()`](https://sqlite.org/c3ref/db_filename.html) + * @since v22.16.0 + * @param dbName Name of the database. This can be `'main'` (the default primary database) or any other + * database that has been added with [`ATTACH DATABASE`](https://www.sqlite.org/lang_attach.html) **Default:** `'main'`. + * @returns The location of the database file. When using an in-memory database, + * this method returns null. + */ + location(dbName?: string): string | null; + /** + * This method allows one or more SQL statements to be executed without returning + * any results. This method is useful when executing SQL statements read from a + * file. This method is a wrapper around [`sqlite3_exec()`](https://www.sqlite.org/c3ref/exec.html). + * @since v22.5.0 + * @param sql A SQL string to execute. + */ + exec(sql: string): void; + /** + * This method is used to create SQLite user-defined functions. This method is a + * wrapper around [`sqlite3_create_function_v2()`](https://www.sqlite.org/c3ref/create_function.html). + * @since v22.13.0 + * @param name The name of the SQLite function to create. + * @param options Optional configuration settings for the function. + * @param func The JavaScript function to call when the SQLite + * function is invoked. The return value of this function should be a valid + * SQLite data type: see + * [Type conversion between JavaScript and SQLite](https://nodejs.org/docs/latest-v22.x/api/sqlite.html#type-conversion-between-javascript-and-sqlite). + * The result defaults to `NULL` if the return value is `undefined`. + */ + function( + name: string, + options: FunctionOptions, + func: (...args: SQLOutputValue[]) => SQLInputValue, + ): void; + function(name: string, func: (...args: SQLOutputValue[]) => SQLInputValue): void; + /** + * Whether the database is currently open or not. + * @since v22.15.0 + */ + readonly isOpen: boolean; + /** + * Whether the database is currently within a transaction. This method + * is a wrapper around [`sqlite3_get_autocommit()`](https://sqlite.org/c3ref/get_autocommit.html). + * @since v22.16.0 + */ + readonly isTransaction: boolean; + /** + * Opens the database specified in the `path` argument of the `DatabaseSync`constructor. This method should only be used when the database is not opened via + * the constructor. An exception is thrown if the database is already open. + * @since v22.5.0 + */ + open(): void; + /** + * Compiles a SQL statement into a [prepared statement](https://www.sqlite.org/c3ref/stmt.html). This method is a wrapper + * around [`sqlite3_prepare_v2()`](https://www.sqlite.org/c3ref/prepare.html). + * @since v22.5.0 + * @param sql A SQL string to compile to a prepared statement. + * @return The prepared statement. + */ + prepare(sql: string): StatementSync; + /** + * Creates and attaches a session to the database. This method is a wrapper around + * [`sqlite3session_create()`](https://www.sqlite.org/session/sqlite3session_create.html) and + * [`sqlite3session_attach()`](https://www.sqlite.org/session/sqlite3session_attach.html). + * @param options The configuration options for the session. + * @returns A session handle. + * @since v22.12.0 + */ + createSession(options?: CreateSessionOptions): Session; + /** + * An exception is thrown if the database is not + * open. This method is a wrapper around + * [`sqlite3changeset_apply()`](https://www.sqlite.org/session/sqlite3changeset_apply.html). + * + * ```js + * const sourceDb = new DatabaseSync(':memory:'); + * const targetDb = new DatabaseSync(':memory:'); + * + * sourceDb.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); + * targetDb.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); + * + * const session = sourceDb.createSession(); + * + * const insert = sourceDb.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); + * insert.run(1, 'hello'); + * insert.run(2, 'world'); + * + * const changeset = session.changeset(); + * targetDb.applyChangeset(changeset); + * // Now that the changeset has been applied, targetDb contains the same data as sourceDb. + * ``` + * @param changeset A binary changeset or patchset. + * @param options The configuration options for how the changes will be applied. + * @returns Whether the changeset was applied successfully without being aborted. + * @since v22.12.0 + */ + applyChangeset(changeset: Uint8Array, options?: ApplyChangesetOptions): boolean; + /** + * Closes the database connection. If the database connection is already closed + * then this is a no-op. + * @since v22.15.0 + * @experimental + */ + [Symbol.dispose](): void; + } + /** + * @since v22.12.0 + */ + interface Session { + /** + * Retrieves a changeset containing all changes since the changeset was created. Can be called multiple times. + * An exception is thrown if the database or the session is not open. This method is a wrapper around + * [`sqlite3session_changeset()`](https://www.sqlite.org/session/sqlite3session_changeset.html). + * @returns Binary changeset that can be applied to other databases. + * @since v22.12.0 + */ + changeset(): NodeJS.NonSharedUint8Array; + /** + * Similar to the method above, but generates a more compact patchset. See + * [Changesets and Patchsets](https://www.sqlite.org/sessionintro.html#changesets_and_patchsets) + * in the documentation of SQLite. An exception is thrown if the database or the session is not open. This method is a + * wrapper around + * [`sqlite3session_patchset()`](https://www.sqlite.org/session/sqlite3session_patchset.html). + * @returns Binary patchset that can be applied to other databases. + * @since v22.12.0 + */ + patchset(): NodeJS.NonSharedUint8Array; + /** + * Closes the session. An exception is thrown if the database or the session is not open. This method is a + * wrapper around + * [`sqlite3session_delete()`](https://www.sqlite.org/session/sqlite3session_delete.html). + */ + close(): void; + } + interface StatementColumnMetadata { + /** + * The unaliased name of the column in the origin + * table, or `null` if the column is the result of an expression or subquery. + * This property is the result of [`sqlite3_column_origin_name()`](https://www.sqlite.org/c3ref/column_database_name.html). + */ + column: string | null; + /** + * The unaliased name of the origin database, or + * `null` if the column is the result of an expression or subquery. This + * property is the result of [`sqlite3_column_database_name()`](https://www.sqlite.org/c3ref/column_database_name.html). + */ + database: string | null; + /** + * The name assigned to the column in the result set of a + * `SELECT` statement. This property is the result of + * [`sqlite3_column_name()`](https://www.sqlite.org/c3ref/column_name.html). + */ + name: string; + /** + * The unaliased name of the origin table, or `null` if + * the column is the result of an expression or subquery. This property is the + * result of [`sqlite3_column_table_name()`](https://www.sqlite.org/c3ref/column_database_name.html). + */ + table: string | null; + /** + * The declared data type of the column, or `null` if the + * column is the result of an expression or subquery. This property is the + * result of [`sqlite3_column_decltype()`](https://www.sqlite.org/c3ref/column_decltype.html). + */ + type: string | null; + } + interface StatementResultingChanges { + /** + * The number of rows modified, inserted, or deleted by the most recently completed `INSERT`, `UPDATE`, or `DELETE` statement. + * This field is either a number or a `BigInt` depending on the prepared statement's configuration. + * This property is the result of [`sqlite3_changes64()`](https://www.sqlite.org/c3ref/changes.html). + */ + changes: number | bigint; + /** + * The most recently inserted rowid. + * This field is either a number or a `BigInt` depending on the prepared statement's configuration. + * This property is the result of [`sqlite3_last_insert_rowid()`](https://www.sqlite.org/c3ref/last_insert_rowid.html). + */ + lastInsertRowid: number | bigint; + } + /** + * This class represents a single [prepared statement](https://www.sqlite.org/c3ref/stmt.html). This class cannot be + * instantiated via its constructor. Instead, instances are created via the`database.prepare()` method. All APIs exposed by this class execute + * synchronously. + * + * A prepared statement is an efficient binary representation of the SQL used to + * create it. Prepared statements are parameterizable, and can be invoked multiple + * times with different bound values. Parameters also offer protection against [SQL injection](https://en.wikipedia.org/wiki/SQL_injection) attacks. For these reasons, prepared statements are + * preferred + * over hand-crafted SQL strings when handling user input. + * @since v22.5.0 + */ + class StatementSync { + private constructor(); + /** + * This method executes a prepared statement and returns all results as an array of + * objects. If the prepared statement does not return any results, this method + * returns an empty array. The prepared statement [parameters are bound](https://www.sqlite.org/c3ref/bind_blob.html) using + * the values in `namedParameters` and `anonymousParameters`. + * @since v22.5.0 + * @param namedParameters An optional object used to bind named parameters. The keys of this object are used to configure the mapping. + * @param anonymousParameters Zero or more values to bind to anonymous parameters. + * @return An array of objects. Each object corresponds to a row returned by executing the prepared statement. The keys and values of each object correspond to the column names and values of + * the row. + */ + all(...anonymousParameters: SQLInputValue[]): Record[]; + all( + namedParameters: Record, + ...anonymousParameters: SQLInputValue[] + ): Record[]; + /** + * This method is used to retrieve information about the columns returned by the + * prepared statement. + * @since v22.16.0 + * @returns An array of objects. Each object corresponds to a column + * in the prepared statement, and contains the following properties: + */ + columns(): StatementColumnMetadata[]; + /** + * The source SQL text of the prepared statement with parameter + * placeholders replaced by the values that were used during the most recent + * execution of this prepared statement. This property is a wrapper around + * [`sqlite3_expanded_sql()`](https://www.sqlite.org/c3ref/expanded_sql.html). + * @since v22.5.0 + */ + readonly expandedSQL: string; + /** + * This method executes a prepared statement and returns the first result as an + * object. If the prepared statement does not return any results, this method + * returns `undefined`. The prepared statement [parameters are bound](https://www.sqlite.org/c3ref/bind_blob.html) using the + * values in `namedParameters` and `anonymousParameters`. + * @since v22.5.0 + * @param namedParameters An optional object used to bind named parameters. The keys of this object are used to configure the mapping. + * @param anonymousParameters Zero or more values to bind to anonymous parameters. + * @return An object corresponding to the first row returned by executing the prepared statement. The keys and values of the object correspond to the column names and values of the row. If no + * rows were returned from the database then this method returns `undefined`. + */ + get(...anonymousParameters: SQLInputValue[]): Record | undefined; + get( + namedParameters: Record, + ...anonymousParameters: SQLInputValue[] + ): Record | undefined; + /** + * This method executes a prepared statement and returns an iterator of + * objects. If the prepared statement does not return any results, this method + * returns an empty iterator. The prepared statement [parameters are bound](https://www.sqlite.org/c3ref/bind_blob.html) using + * the values in `namedParameters` and `anonymousParameters`. + * @since v22.13.0 + * @param namedParameters An optional object used to bind named parameters. + * The keys of this object are used to configure the mapping. + * @param anonymousParameters Zero or more values to bind to anonymous parameters. + * @returns An iterable iterator of objects. Each object corresponds to a row + * returned by executing the prepared statement. The keys and values of each + * object correspond to the column names and values of the row. + */ + iterate(...anonymousParameters: SQLInputValue[]): NodeJS.Iterator>; + iterate( + namedParameters: Record, + ...anonymousParameters: SQLInputValue[] + ): NodeJS.Iterator>; + /** + * This method executes a prepared statement and returns an object summarizing the + * resulting changes. The prepared statement [parameters are bound](https://www.sqlite.org/c3ref/bind_blob.html) using the + * values in `namedParameters` and `anonymousParameters`. + * @since v22.5.0 + * @param namedParameters An optional object used to bind named parameters. The keys of this object are used to configure the mapping. + * @param anonymousParameters Zero or more values to bind to anonymous parameters. + */ + run(...anonymousParameters: SQLInputValue[]): StatementResultingChanges; + run( + namedParameters: Record, + ...anonymousParameters: SQLInputValue[] + ): StatementResultingChanges; + /** + * The names of SQLite parameters begin with a prefix character. By default,`node:sqlite` requires that this prefix character is present when binding + * parameters. However, with the exception of dollar sign character, these + * prefix characters also require extra quoting when used in object keys. + * + * To improve ergonomics, this method can be used to also allow bare named + * parameters, which do not require the prefix character in JavaScript code. There + * are several caveats to be aware of when enabling bare named parameters: + * + * * The prefix character is still required in SQL. + * * The prefix character is still allowed in JavaScript. In fact, prefixed names + * will have slightly better binding performance. + * * Using ambiguous named parameters, such as `$k` and `@k`, in the same prepared + * statement will result in an exception as it cannot be determined how to bind + * a bare name. + * @since v22.5.0 + * @param enabled Enables or disables support for binding named parameters without the prefix character. + */ + setAllowBareNamedParameters(enabled: boolean): void; + /** + * By default, if an unknown name is encountered while binding parameters, an + * exception is thrown. This method allows unknown named parameters to be ignored. + * @since v22.15.0 + * @param enabled Enables or disables support for unknown named parameters. + */ + setAllowUnknownNamedParameters(enabled: boolean): void; + /** + * When enabled, query results returned by the `all()`, `get()`, and `iterate()` methods will be returned as arrays instead + * of objects. + * @since v22.16.0 + * @param enabled Enables or disables the return of query results as arrays. + */ + setReturnArrays(enabled: boolean): void; + /** + * When reading from the database, SQLite `INTEGER`s are mapped to JavaScript + * numbers by default. However, SQLite `INTEGER`s can store values larger than + * JavaScript numbers are capable of representing. In such cases, this method can + * be used to read `INTEGER` data using JavaScript `BigInt`s. This method has no + * impact on database write operations where numbers and `BigInt`s are both + * supported at all times. + * @since v22.5.0 + * @param enabled Enables or disables the use of `BigInt`s when reading `INTEGER` fields from the database. + */ + setReadBigInts(enabled: boolean): void; + /** + * The source SQL text of the prepared statement. This property is a + * wrapper around [`sqlite3_sql()`](https://www.sqlite.org/c3ref/expanded_sql.html). + * @since v22.5.0 + */ + readonly sourceSQL: string; + } + interface BackupOptions { + /** + * Name of the source database. This can be `'main'` (the default primary database) or any other + * database that have been added with [`ATTACH DATABASE`](https://www.sqlite.org/lang_attach.html) + * @default 'main' + */ + source?: string | undefined; + /** + * Name of the target database. This can be `'main'` (the default primary database) or any other + * database that have been added with [`ATTACH DATABASE`](https://www.sqlite.org/lang_attach.html) + * @default 'main' + */ + target?: string | undefined; + /** + * Number of pages to be transmitted in each batch of the backup. + * @default 100 + */ + rate?: number | undefined; + /** + * Callback function that will be called with the number of pages copied and the total number of + * pages. + */ + progress?: ((progressInfo: BackupProgressInfo) => void) | undefined; + } + interface BackupProgressInfo { + totalPages: number; + remainingPages: number; + } + /** + * This method makes a database backup. This method abstracts the + * [`sqlite3_backup_init()`](https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit), + * [`sqlite3_backup_step()`](https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep) + * and [`sqlite3_backup_finish()`](https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish) functions. + * + * The backed-up database can be used normally during the backup process. Mutations coming from the same connection - same + * `DatabaseSync` - object will be reflected in the backup right away. However, mutations from other connections will cause + * the backup process to restart. + * + * ```js + * import { backup, DatabaseSync } from 'node:sqlite'; + * + * const sourceDb = new DatabaseSync('source.db'); + * const totalPagesTransferred = await backup(sourceDb, 'backup.db', { + * rate: 1, // Copy one page at a time. + * progress: ({ totalPages, remainingPages }) => { + * console.log('Backup in progress', { totalPages, remainingPages }); + * }, + * }); + * + * console.log('Backup completed', totalPagesTransferred); + * ``` + * @since v22.16.0 + * @param sourceDb The database to backup. The source database must be open. + * @param path The path where the backup will be created. If the file already exists, + * the contents will be overwritten. + * @param options Optional configuration for the backup. The + * following properties are supported: + * @returns A promise that resolves when the backup is completed and rejects if an error occurs. + */ + function backup(sourceDb: DatabaseSync, path: PathLike, options?: BackupOptions): Promise; + /** + * @since v22.13.0 + */ + namespace constants { + /** + * The conflict handler is invoked with this constant when processing a DELETE or UPDATE change if a row with the required PRIMARY KEY fields is present in the database, but one or more other (non primary-key) fields modified by the update do not contain the expected "before" values. + * @since v22.14.0 + */ + const SQLITE_CHANGESET_DATA: number; + /** + * The conflict handler is invoked with this constant when processing a DELETE or UPDATE change if a row with the required PRIMARY KEY fields is not present in the database. + * @since v22.14.0 + */ + const SQLITE_CHANGESET_NOTFOUND: number; + /** + * This constant is passed to the conflict handler while processing an INSERT change if the operation would result in duplicate primary key values. + * @since v22.14.0 + */ + const SQLITE_CHANGESET_CONFLICT: number; + /** + * If foreign key handling is enabled, and applying a changeset leaves the database in a state containing foreign key violations, the conflict handler is invoked with this constant exactly once before the changeset is committed. If the conflict handler returns `SQLITE_CHANGESET_OMIT`, the changes, including those that caused the foreign key constraint violation, are committed. Or, if it returns `SQLITE_CHANGESET_ABORT`, the changeset is rolled back. + * @since v22.14.0 + */ + const SQLITE_CHANGESET_FOREIGN_KEY: number; + /** + * Conflicting changes are omitted. + * @since v22.12.0 + */ + const SQLITE_CHANGESET_OMIT: number; + /** + * Conflicting changes replace existing values. Note that this value can only be returned when the type of conflict is either `SQLITE_CHANGESET_DATA` or `SQLITE_CHANGESET_CONFLICT`. + * @since v22.12.0 + */ + const SQLITE_CHANGESET_REPLACE: number; + /** + * Abort when a change encounters a conflict and roll back database. + * @since v22.12.0 + */ + const SQLITE_CHANGESET_ABORT: number; + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream.d.ts new file mode 100644 index 00000000..5799cc03 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream.d.ts @@ -0,0 +1,1664 @@ +/** + * A stream is an abstract interface for working with streaming data in Node.js. + * The `node:stream` module provides an API for implementing the stream interface. + * + * There are many stream objects provided by Node.js. For instance, a [request to an HTTP server](https://nodejs.org/docs/latest-v22.x/api/http.html#class-httpincomingmessage) + * and [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) are both stream instances. + * + * Streams can be readable, writable, or both. All streams are instances of [`EventEmitter`](https://nodejs.org/docs/latest-v22.x/api/events.html#class-eventemitter). + * + * To access the `node:stream` module: + * + * ```js + * import stream from 'node:stream'; + * ``` + * + * The `node:stream` module is useful for creating new types of stream instances. + * It is usually not necessary to use the `node:stream` module to consume streams. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/stream.js) + */ +declare module "stream" { + import { Abortable, EventEmitter } from "node:events"; + import { Blob as NodeBlob } from "node:buffer"; + import * as streamPromises from "node:stream/promises"; + import * as streamWeb from "node:stream/web"; + + type ComposeFnParam = (source: any) => void; + + class Stream extends EventEmitter { + pipe( + destination: T, + options?: { + end?: boolean | undefined; + }, + ): T; + compose( + stream: T | ComposeFnParam | Iterable | AsyncIterable, + options?: { signal: AbortSignal }, + ): T; + } + namespace Stream { + export { Stream, streamPromises as promises }; + } + namespace Stream { + interface StreamOptions extends Abortable { + emitClose?: boolean | undefined; + highWaterMark?: number | undefined; + objectMode?: boolean | undefined; + construct?: ((this: T, callback: (error?: Error | null) => void) => void) | undefined; + destroy?: ((this: T, error: Error | null, callback: (error?: Error | null) => void) => void) | undefined; + autoDestroy?: boolean | undefined; + } + interface ReadableOptions extends StreamOptions { + encoding?: BufferEncoding | undefined; + read?: ((this: T, size: number) => void) | undefined; + } + interface ArrayOptions { + /** + * The maximum concurrent invocations of `fn` to call on the stream at once. + * @default 1 + */ + concurrency?: number | undefined; + /** Allows destroying the stream if the signal is aborted. */ + signal?: AbortSignal | undefined; + } + /** + * @since v0.9.4 + */ + class Readable extends Stream implements NodeJS.ReadableStream { + /** + * A utility method for creating Readable Streams out of iterators. + * @since v12.3.0, v10.17.0 + * @param iterable Object implementing the `Symbol.asyncIterator` or `Symbol.iterator` iterable protocol. Emits an 'error' event if a null value is passed. + * @param options Options provided to `new stream.Readable([options])`. By default, `Readable.from()` will set `options.objectMode` to `true`, unless this is explicitly opted out by setting `options.objectMode` to `false`. + */ + static from(iterable: Iterable | AsyncIterable, options?: ReadableOptions): Readable; + /** + * A utility method for creating a `Readable` from a web `ReadableStream`. + * @since v17.0.0 + */ + static fromWeb( + readableStream: streamWeb.ReadableStream, + options?: Pick, + ): Readable; + /** + * A utility method for creating a web `ReadableStream` from a `Readable`. + * @since v17.0.0 + */ + static toWeb( + streamReadable: Readable, + options?: { + strategy?: streamWeb.QueuingStrategy | undefined; + }, + ): streamWeb.ReadableStream; + /** + * Returns whether the stream has been read from or cancelled. + * @since v16.8.0 + */ + static isDisturbed(stream: Readable | NodeJS.ReadableStream): boolean; + /** + * Returns whether the stream was destroyed or errored before emitting `'end'`. + * @since v16.8.0 + */ + readonly readableAborted: boolean; + /** + * Is `true` if it is safe to call {@link read}, which means + * the stream has not been destroyed or emitted `'error'` or `'end'`. + * @since v11.4.0 + */ + readable: boolean; + /** + * Returns whether `'data'` has been emitted. + * @since v16.7.0, v14.18.0 + */ + readonly readableDidRead: boolean; + /** + * Getter for the property `encoding` of a given `Readable` stream. The `encoding` property can be set using the {@link setEncoding} method. + * @since v12.7.0 + */ + readonly readableEncoding: BufferEncoding | null; + /** + * Becomes `true` when [`'end'`](https://nodejs.org/docs/latest-v22.x/api/stream.html#event-end) event is emitted. + * @since v12.9.0 + */ + readonly readableEnded: boolean; + /** + * This property reflects the current state of a `Readable` stream as described + * in the [Three states](https://nodejs.org/docs/latest-v22.x/api/stream.html#three-states) section. + * @since v9.4.0 + */ + readonly readableFlowing: boolean | null; + /** + * Returns the value of `highWaterMark` passed when creating this `Readable`. + * @since v9.3.0 + */ + readonly readableHighWaterMark: number; + /** + * This property contains the number of bytes (or objects) in the queue + * ready to be read. The value provides introspection data regarding + * the status of the `highWaterMark`. + * @since v9.4.0 + */ + readonly readableLength: number; + /** + * Getter for the property `objectMode` of a given `Readable` stream. + * @since v12.3.0 + */ + readonly readableObjectMode: boolean; + /** + * Is `true` after `readable.destroy()` has been called. + * @since v8.0.0 + */ + destroyed: boolean; + /** + * Is `true` after `'close'` has been emitted. + * @since v18.0.0 + */ + readonly closed: boolean; + /** + * Returns error if the stream has been destroyed with an error. + * @since v18.0.0 + */ + readonly errored: Error | null; + constructor(opts?: ReadableOptions); + _construct?(callback: (error?: Error | null) => void): void; + _read(size: number): void; + /** + * The `readable.read()` method reads data out of the internal buffer and + * returns it. If no data is available to be read, `null` is returned. By default, + * the data is returned as a `Buffer` object unless an encoding has been + * specified using the `readable.setEncoding()` method or the stream is operating + * in object mode. + * + * The optional `size` argument specifies a specific number of bytes to read. If + * `size` bytes are not available to be read, `null` will be returned _unless_ the + * stream has ended, in which case all of the data remaining in the internal buffer + * will be returned. + * + * If the `size` argument is not specified, all of the data contained in the + * internal buffer will be returned. + * + * The `size` argument must be less than or equal to 1 GiB. + * + * The `readable.read()` method should only be called on `Readable` streams + * operating in paused mode. In flowing mode, `readable.read()` is called + * automatically until the internal buffer is fully drained. + * + * ```js + * const readable = getReadableStreamSomehow(); + * + * // 'readable' may be triggered multiple times as data is buffered in + * readable.on('readable', () => { + * let chunk; + * console.log('Stream is readable (new data received in buffer)'); + * // Use a loop to make sure we read all currently available data + * while (null !== (chunk = readable.read())) { + * console.log(`Read ${chunk.length} bytes of data...`); + * } + * }); + * + * // 'end' will be triggered once when there is no more data available + * readable.on('end', () => { + * console.log('Reached end of stream.'); + * }); + * ``` + * + * Each call to `readable.read()` returns a chunk of data, or `null`. The chunks + * are not concatenated. A `while` loop is necessary to consume all data + * currently in the buffer. When reading a large file `.read()` may return `null`, + * having consumed all buffered content so far, but there is still more data to + * come not yet buffered. In this case a new `'readable'` event will be emitted + * when there is more data in the buffer. Finally the `'end'` event will be + * emitted when there is no more data to come. + * + * Therefore to read a file's whole contents from a `readable`, it is necessary + * to collect chunks across multiple `'readable'` events: + * + * ```js + * const chunks = []; + * + * readable.on('readable', () => { + * let chunk; + * while (null !== (chunk = readable.read())) { + * chunks.push(chunk); + * } + * }); + * + * readable.on('end', () => { + * const content = chunks.join(''); + * }); + * ``` + * + * A `Readable` stream in object mode will always return a single item from + * a call to `readable.read(size)`, regardless of the value of the `size` argument. + * + * If the `readable.read()` method returns a chunk of data, a `'data'` event will + * also be emitted. + * + * Calling {@link read} after the `'end'` event has + * been emitted will return `null`. No runtime error will be raised. + * @since v0.9.4 + * @param size Optional argument to specify how much data to read. + */ + read(size?: number): any; + /** + * The `readable.setEncoding()` method sets the character encoding for + * data read from the `Readable` stream. + * + * By default, no encoding is assigned and stream data will be returned as `Buffer` objects. Setting an encoding causes the stream data + * to be returned as strings of the specified encoding rather than as `Buffer` objects. For instance, calling `readable.setEncoding('utf8')` will cause the + * output data to be interpreted as UTF-8 data, and passed as strings. Calling `readable.setEncoding('hex')` will cause the data to be encoded in hexadecimal + * string format. + * + * The `Readable` stream will properly handle multi-byte characters delivered + * through the stream that would otherwise become improperly decoded if simply + * pulled from the stream as `Buffer` objects. + * + * ```js + * const readable = getReadableStreamSomehow(); + * readable.setEncoding('utf8'); + * readable.on('data', (chunk) => { + * assert.equal(typeof chunk, 'string'); + * console.log('Got %d characters of string data:', chunk.length); + * }); + * ``` + * @since v0.9.4 + * @param encoding The encoding to use. + */ + setEncoding(encoding: BufferEncoding): this; + /** + * The `readable.pause()` method will cause a stream in flowing mode to stop + * emitting `'data'` events, switching out of flowing mode. Any data that + * becomes available will remain in the internal buffer. + * + * ```js + * const readable = getReadableStreamSomehow(); + * readable.on('data', (chunk) => { + * console.log(`Received ${chunk.length} bytes of data.`); + * readable.pause(); + * console.log('There will be no additional data for 1 second.'); + * setTimeout(() => { + * console.log('Now data will start flowing again.'); + * readable.resume(); + * }, 1000); + * }); + * ``` + * + * The `readable.pause()` method has no effect if there is a `'readable'` event listener. + * @since v0.9.4 + */ + pause(): this; + /** + * The `readable.resume()` method causes an explicitly paused `Readable` stream to + * resume emitting `'data'` events, switching the stream into flowing mode. + * + * The `readable.resume()` method can be used to fully consume the data from a + * stream without actually processing any of that data: + * + * ```js + * getReadableStreamSomehow() + * .resume() + * .on('end', () => { + * console.log('Reached the end, but did not read anything.'); + * }); + * ``` + * + * The `readable.resume()` method has no effect if there is a `'readable'` event listener. + * @since v0.9.4 + */ + resume(): this; + /** + * The `readable.isPaused()` method returns the current operating state of the `Readable`. + * This is used primarily by the mechanism that underlies the `readable.pipe()` method. + * In most typical cases, there will be no reason to use this method directly. + * + * ```js + * const readable = new stream.Readable(); + * + * readable.isPaused(); // === false + * readable.pause(); + * readable.isPaused(); // === true + * readable.resume(); + * readable.isPaused(); // === false + * ``` + * @since v0.11.14 + */ + isPaused(): boolean; + /** + * The `readable.unpipe()` method detaches a `Writable` stream previously attached + * using the {@link pipe} method. + * + * If the `destination` is not specified, then _all_ pipes are detached. + * + * If the `destination` is specified, but no pipe is set up for it, then + * the method does nothing. + * + * ```js + * import fs from 'node:fs'; + * const readable = getReadableStreamSomehow(); + * const writable = fs.createWriteStream('file.txt'); + * // All the data from readable goes into 'file.txt', + * // but only for the first second. + * readable.pipe(writable); + * setTimeout(() => { + * console.log('Stop writing to file.txt.'); + * readable.unpipe(writable); + * console.log('Manually close the file stream.'); + * writable.end(); + * }, 1000); + * ``` + * @since v0.9.4 + * @param destination Optional specific stream to unpipe + */ + unpipe(destination?: NodeJS.WritableStream): this; + /** + * Passing `chunk` as `null` signals the end of the stream (EOF) and behaves the + * same as `readable.push(null)`, after which no more data can be written. The EOF + * signal is put at the end of the buffer and any buffered data will still be + * flushed. + * + * The `readable.unshift()` method pushes a chunk of data back into the internal + * buffer. This is useful in certain situations where a stream is being consumed by + * code that needs to "un-consume" some amount of data that it has optimistically + * pulled out of the source, so that the data can be passed on to some other party. + * + * The `stream.unshift(chunk)` method cannot be called after the `'end'` event + * has been emitted or a runtime error will be thrown. + * + * Developers using `stream.unshift()` often should consider switching to + * use of a `Transform` stream instead. See the `API for stream implementers` section for more information. + * + * ```js + * // Pull off a header delimited by \n\n. + * // Use unshift() if we get too much. + * // Call the callback with (error, header, stream). + * import { StringDecoder } from 'node:string_decoder'; + * function parseHeader(stream, callback) { + * stream.on('error', callback); + * stream.on('readable', onReadable); + * const decoder = new StringDecoder('utf8'); + * let header = ''; + * function onReadable() { + * let chunk; + * while (null !== (chunk = stream.read())) { + * const str = decoder.write(chunk); + * if (str.includes('\n\n')) { + * // Found the header boundary. + * const split = str.split(/\n\n/); + * header += split.shift(); + * const remaining = split.join('\n\n'); + * const buf = Buffer.from(remaining, 'utf8'); + * stream.removeListener('error', callback); + * // Remove the 'readable' listener before unshifting. + * stream.removeListener('readable', onReadable); + * if (buf.length) + * stream.unshift(buf); + * // Now the body of the message can be read from the stream. + * callback(null, header, stream); + * return; + * } + * // Still reading the header. + * header += str; + * } + * } + * } + * ``` + * + * Unlike {@link push}, `stream.unshift(chunk)` will not + * end the reading process by resetting the internal reading state of the stream. + * This can cause unexpected results if `readable.unshift()` is called during a + * read (i.e. from within a {@link _read} implementation on a + * custom stream). Following the call to `readable.unshift()` with an immediate {@link push} will reset the reading state appropriately, + * however it is best to simply avoid calling `readable.unshift()` while in the + * process of performing a read. + * @since v0.9.11 + * @param chunk Chunk of data to unshift onto the read queue. For streams not operating in object mode, `chunk` must + * be a {string}, {Buffer}, {TypedArray}, {DataView} or `null`. For object mode streams, `chunk` may be any JavaScript value. + * @param encoding Encoding of string chunks. Must be a valid `Buffer` encoding, such as `'utf8'` or `'ascii'`. + */ + unshift(chunk: any, encoding?: BufferEncoding): void; + /** + * Prior to Node.js 0.10, streams did not implement the entire `node:stream` module API as it is currently defined. (See `Compatibility` for more + * information.) + * + * When using an older Node.js library that emits `'data'` events and has a {@link pause} method that is advisory only, the `readable.wrap()` method can be used to create a `Readable` + * stream that uses + * the old stream as its data source. + * + * It will rarely be necessary to use `readable.wrap()` but the method has been + * provided as a convenience for interacting with older Node.js applications and + * libraries. + * + * ```js + * import { OldReader } from './old-api-module.js'; + * import { Readable } from 'node:stream'; + * const oreader = new OldReader(); + * const myReader = new Readable().wrap(oreader); + * + * myReader.on('readable', () => { + * myReader.read(); // etc. + * }); + * ``` + * @since v0.9.4 + * @param stream An "old style" readable stream + */ + wrap(stream: NodeJS.ReadableStream): this; + push(chunk: any, encoding?: BufferEncoding): boolean; + /** + * The iterator created by this method gives users the option to cancel the destruction + * of the stream if the `for await...of` loop is exited by `return`, `break`, or `throw`, + * or if the iterator should destroy the stream if the stream emitted an error during iteration. + * @since v16.3.0 + * @param options.destroyOnReturn When set to `false`, calling `return` on the async iterator, + * or exiting a `for await...of` iteration using a `break`, `return`, or `throw` will not destroy the stream. + * **Default: `true`**. + */ + iterator(options?: { destroyOnReturn?: boolean }): NodeJS.AsyncIterator; + /** + * This method allows mapping over the stream. The *fn* function will be called for every chunk in the stream. + * If the *fn* function returns a promise - that promise will be `await`ed before being passed to the result stream. + * @since v17.4.0, v16.14.0 + * @param fn a function to map over every chunk in the stream. Async or not. + * @returns a stream mapped with the function *fn*. + */ + map(fn: (data: any, options?: Pick) => any, options?: ArrayOptions): Readable; + /** + * This method allows filtering the stream. For each chunk in the stream the *fn* function will be called + * and if it returns a truthy value, the chunk will be passed to the result stream. + * If the *fn* function returns a promise - that promise will be `await`ed. + * @since v17.4.0, v16.14.0 + * @param fn a function to filter chunks from the stream. Async or not. + * @returns a stream filtered with the predicate *fn*. + */ + filter( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Readable; + /** + * This method allows iterating a stream. For each chunk in the stream the *fn* function will be called. + * If the *fn* function returns a promise - that promise will be `await`ed. + * + * This method is different from `for await...of` loops in that it can optionally process chunks concurrently. + * In addition, a `forEach` iteration can only be stopped by having passed a `signal` option + * and aborting the related AbortController while `for await...of` can be stopped with `break` or `return`. + * In either case the stream will be destroyed. + * + * This method is different from listening to the `'data'` event in that it uses the `readable` event + * in the underlying machinary and can limit the number of concurrent *fn* calls. + * @since v17.5.0 + * @param fn a function to call on each chunk of the stream. Async or not. + * @returns a promise for when the stream has finished. + */ + forEach( + fn: (data: any, options?: Pick) => void | Promise, + options?: ArrayOptions, + ): Promise; + /** + * This method allows easily obtaining the contents of a stream. + * + * As this method reads the entire stream into memory, it negates the benefits of streams. It's intended + * for interoperability and convenience, not as the primary way to consume streams. + * @since v17.5.0 + * @returns a promise containing an array with the contents of the stream. + */ + toArray(options?: Pick): Promise; + /** + * This method is similar to `Array.prototype.some` and calls *fn* on each chunk in the stream + * until the awaited return value is `true` (or any truthy value). Once an *fn* call on a chunk + * `await`ed return value is truthy, the stream is destroyed and the promise is fulfilled with `true`. + * If none of the *fn* calls on the chunks return a truthy value, the promise is fulfilled with `false`. + * @since v17.5.0 + * @param fn a function to call on each chunk of the stream. Async or not. + * @returns a promise evaluating to `true` if *fn* returned a truthy value for at least one of the chunks. + */ + some( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Promise; + /** + * This method is similar to `Array.prototype.find` and calls *fn* on each chunk in the stream + * to find a chunk with a truthy value for *fn*. Once an *fn* call's awaited return value is truthy, + * the stream is destroyed and the promise is fulfilled with value for which *fn* returned a truthy value. + * If all of the *fn* calls on the chunks return a falsy value, the promise is fulfilled with `undefined`. + * @since v17.5.0 + * @param fn a function to call on each chunk of the stream. Async or not. + * @returns a promise evaluating to the first chunk for which *fn* evaluated with a truthy value, + * or `undefined` if no element was found. + */ + find( + fn: (data: any, options?: Pick) => data is T, + options?: ArrayOptions, + ): Promise; + find( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Promise; + /** + * This method is similar to `Array.prototype.every` and calls *fn* on each chunk in the stream + * to check if all awaited return values are truthy value for *fn*. Once an *fn* call on a chunk + * `await`ed return value is falsy, the stream is destroyed and the promise is fulfilled with `false`. + * If all of the *fn* calls on the chunks return a truthy value, the promise is fulfilled with `true`. + * @since v17.5.0 + * @param fn a function to call on each chunk of the stream. Async or not. + * @returns a promise evaluating to `true` if *fn* returned a truthy value for every one of the chunks. + */ + every( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Promise; + /** + * This method returns a new stream by applying the given callback to each chunk of the stream + * and then flattening the result. + * + * It is possible to return a stream or another iterable or async iterable from *fn* and the result streams + * will be merged (flattened) into the returned stream. + * @since v17.5.0 + * @param fn a function to map over every chunk in the stream. May be async. May be a stream or generator. + * @returns a stream flat-mapped with the function *fn*. + */ + flatMap(fn: (data: any, options?: Pick) => any, options?: ArrayOptions): Readable; + /** + * This method returns a new stream with the first *limit* chunks dropped from the start. + * @since v17.5.0 + * @param limit the number of chunks to drop from the readable. + * @returns a stream with *limit* chunks dropped from the start. + */ + drop(limit: number, options?: Pick): Readable; + /** + * This method returns a new stream with the first *limit* chunks. + * @since v17.5.0 + * @param limit the number of chunks to take from the readable. + * @returns a stream with *limit* chunks taken. + */ + take(limit: number, options?: Pick): Readable; + /** + * This method returns a new stream with chunks of the underlying stream paired with a counter + * in the form `[index, chunk]`. The first index value is `0` and it increases by 1 for each chunk produced. + * @since v17.5.0 + * @returns a stream of indexed pairs. + */ + asIndexedPairs(options?: Pick): Readable; + /** + * This method calls *fn* on each chunk of the stream in order, passing it the result from the calculation + * on the previous element. It returns a promise for the final value of the reduction. + * + * If no *initial* value is supplied the first chunk of the stream is used as the initial value. + * If the stream is empty, the promise is rejected with a `TypeError` with the `ERR_INVALID_ARGS` code property. + * + * The reducer function iterates the stream element-by-element which means that there is no *concurrency* parameter + * or parallelism. To perform a reduce concurrently, you can extract the async function to `readable.map` method. + * @since v17.5.0 + * @param fn a reducer function to call over every chunk in the stream. Async or not. + * @param initial the initial value to use in the reduction. + * @returns a promise for the final value of the reduction. + */ + reduce( + fn: (previous: any, data: any, options?: Pick) => T, + initial?: undefined, + options?: Pick, + ): Promise; + reduce( + fn: (previous: T, data: any, options?: Pick) => T, + initial: T, + options?: Pick, + ): Promise; + _destroy(error: Error | null, callback: (error?: Error | null) => void): void; + /** + * Destroy the stream. Optionally emit an `'error'` event, and emit a `'close'` event (unless `emitClose` is set to `false`). After this call, the readable + * stream will release any internal resources and subsequent calls to `push()` will be ignored. + * + * Once `destroy()` has been called any further calls will be a no-op and no + * further errors except from `_destroy()` may be emitted as `'error'`. + * + * Implementors should not override this method, but instead implement `readable._destroy()`. + * @since v8.0.0 + * @param error Error which will be passed as payload in `'error'` event + */ + destroy(error?: Error): this; + /** + * Event emitter + * The defined events on documents including: + * 1. close + * 2. data + * 3. end + * 4. error + * 5. pause + * 6. readable + * 7. resume + */ + addListener(event: "close", listener: () => void): this; + addListener(event: "data", listener: (chunk: any) => void): this; + addListener(event: "end", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "pause", listener: () => void): this; + addListener(event: "readable", listener: () => void): this; + addListener(event: "resume", listener: () => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "close"): boolean; + emit(event: "data", chunk: any): boolean; + emit(event: "end"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "pause"): boolean; + emit(event: "readable"): boolean; + emit(event: "resume"): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "close", listener: () => void): this; + on(event: "data", listener: (chunk: any) => void): this; + on(event: "end", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "pause", listener: () => void): this; + on(event: "readable", listener: () => void): this; + on(event: "resume", listener: () => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "data", listener: (chunk: any) => void): this; + once(event: "end", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "pause", listener: () => void): this; + once(event: "readable", listener: () => void): this; + once(event: "resume", listener: () => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "data", listener: (chunk: any) => void): this; + prependListener(event: "end", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "pause", listener: () => void): this; + prependListener(event: "readable", listener: () => void): this; + prependListener(event: "resume", listener: () => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "data", listener: (chunk: any) => void): this; + prependOnceListener(event: "end", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "pause", listener: () => void): this; + prependOnceListener(event: "readable", listener: () => void): this; + prependOnceListener(event: "resume", listener: () => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + removeListener(event: "close", listener: () => void): this; + removeListener(event: "data", listener: (chunk: any) => void): this; + removeListener(event: "end", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: "pause", listener: () => void): this; + removeListener(event: "readable", listener: () => void): this; + removeListener(event: "resume", listener: () => void): this; + removeListener(event: string | symbol, listener: (...args: any[]) => void): this; + [Symbol.asyncIterator](): NodeJS.AsyncIterator; + /** + * Calls `readable.destroy()` with an `AbortError` and returns a promise that fulfills when the stream is finished. + * @since v20.4.0 + */ + [Symbol.asyncDispose](): Promise; + } + interface WritableOptions extends StreamOptions { + decodeStrings?: boolean | undefined; + defaultEncoding?: BufferEncoding | undefined; + write?: + | (( + this: T, + chunk: any, + encoding: BufferEncoding, + callback: (error?: Error | null) => void, + ) => void) + | undefined; + writev?: + | (( + this: T, + chunks: Array<{ + chunk: any; + encoding: BufferEncoding; + }>, + callback: (error?: Error | null) => void, + ) => void) + | undefined; + final?: ((this: T, callback: (error?: Error | null) => void) => void) | undefined; + } + /** + * @since v0.9.4 + */ + class Writable extends Stream implements NodeJS.WritableStream { + /** + * A utility method for creating a `Writable` from a web `WritableStream`. + * @since v17.0.0 + */ + static fromWeb( + writableStream: streamWeb.WritableStream, + options?: Pick, + ): Writable; + /** + * A utility method for creating a web `WritableStream` from a `Writable`. + * @since v17.0.0 + */ + static toWeb(streamWritable: Writable): streamWeb.WritableStream; + /** + * Is `true` if it is safe to call `writable.write()`, which means + * the stream has not been destroyed, errored, or ended. + * @since v11.4.0 + */ + readonly writable: boolean; + /** + * Returns whether the stream was destroyed or errored before emitting `'finish'`. + * @since v18.0.0, v16.17.0 + */ + readonly writableAborted: boolean; + /** + * Is `true` after `writable.end()` has been called. This property + * does not indicate whether the data has been flushed, for this use `writable.writableFinished` instead. + * @since v12.9.0 + */ + readonly writableEnded: boolean; + /** + * Is set to `true` immediately before the `'finish'` event is emitted. + * @since v12.6.0 + */ + readonly writableFinished: boolean; + /** + * Return the value of `highWaterMark` passed when creating this `Writable`. + * @since v9.3.0 + */ + readonly writableHighWaterMark: number; + /** + * This property contains the number of bytes (or objects) in the queue + * ready to be written. The value provides introspection data regarding + * the status of the `highWaterMark`. + * @since v9.4.0 + */ + readonly writableLength: number; + /** + * Getter for the property `objectMode` of a given `Writable` stream. + * @since v12.3.0 + */ + readonly writableObjectMode: boolean; + /** + * Number of times `writable.uncork()` needs to be + * called in order to fully uncork the stream. + * @since v13.2.0, v12.16.0 + */ + readonly writableCorked: number; + /** + * Is `true` after `writable.destroy()` has been called. + * @since v8.0.0 + */ + destroyed: boolean; + /** + * Is `true` after `'close'` has been emitted. + * @since v18.0.0 + */ + readonly closed: boolean; + /** + * Returns error if the stream has been destroyed with an error. + * @since v18.0.0 + */ + readonly errored: Error | null; + /** + * Is `true` if the stream's buffer has been full and stream will emit `'drain'`. + * @since v15.2.0, v14.17.0 + */ + readonly writableNeedDrain: boolean; + constructor(opts?: WritableOptions); + _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void; + _writev?( + chunks: Array<{ + chunk: any; + encoding: BufferEncoding; + }>, + callback: (error?: Error | null) => void, + ): void; + _construct?(callback: (error?: Error | null) => void): void; + _destroy(error: Error | null, callback: (error?: Error | null) => void): void; + _final(callback: (error?: Error | null) => void): void; + /** + * The `writable.write()` method writes some data to the stream, and calls the + * supplied `callback` once the data has been fully handled. If an error + * occurs, the `callback` will be called with the error as its + * first argument. The `callback` is called asynchronously and before `'error'` is + * emitted. + * + * The return value is `true` if the internal buffer is less than the `highWaterMark` configured when the stream was created after admitting `chunk`. + * If `false` is returned, further attempts to write data to the stream should + * stop until the `'drain'` event is emitted. + * + * While a stream is not draining, calls to `write()` will buffer `chunk`, and + * return false. Once all currently buffered chunks are drained (accepted for + * delivery by the operating system), the `'drain'` event will be emitted. + * Once `write()` returns false, do not write more chunks + * until the `'drain'` event is emitted. While calling `write()` on a stream that + * is not draining is allowed, Node.js will buffer all written chunks until + * maximum memory usage occurs, at which point it will abort unconditionally. + * Even before it aborts, high memory usage will cause poor garbage collector + * performance and high RSS (which is not typically released back to the system, + * even after the memory is no longer required). Since TCP sockets may never + * drain if the remote peer does not read the data, writing a socket that is + * not draining may lead to a remotely exploitable vulnerability. + * + * Writing data while the stream is not draining is particularly + * problematic for a `Transform`, because the `Transform` streams are paused + * by default until they are piped or a `'data'` or `'readable'` event handler + * is added. + * + * If the data to be written can be generated or fetched on demand, it is + * recommended to encapsulate the logic into a `Readable` and use {@link pipe}. However, if calling `write()` is preferred, it is + * possible to respect backpressure and avoid memory issues using the `'drain'` event: + * + * ```js + * function write(data, cb) { + * if (!stream.write(data)) { + * stream.once('drain', cb); + * } else { + * process.nextTick(cb); + * } + * } + * + * // Wait for cb to be called before doing any other write. + * write('hello', () => { + * console.log('Write completed, do more writes now.'); + * }); + * ``` + * + * A `Writable` stream in object mode will always ignore the `encoding` argument. + * @since v0.9.4 + * @param chunk Optional data to write. For streams not operating in object mode, `chunk` must be a {string}, {Buffer}, + * {TypedArray} or {DataView}. For object mode streams, `chunk` may be any JavaScript value other than `null`. + * @param [encoding='utf8'] The encoding, if `chunk` is a string. + * @param callback Callback for when this chunk of data is flushed. + * @return `false` if the stream wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + write(chunk: any, callback?: (error: Error | null | undefined) => void): boolean; + write(chunk: any, encoding: BufferEncoding, callback?: (error: Error | null | undefined) => void): boolean; + /** + * The `writable.setDefaultEncoding()` method sets the default `encoding` for a `Writable` stream. + * @since v0.11.15 + * @param encoding The new default encoding + */ + setDefaultEncoding(encoding: BufferEncoding): this; + /** + * Calling the `writable.end()` method signals that no more data will be written + * to the `Writable`. The optional `chunk` and `encoding` arguments allow one + * final additional chunk of data to be written immediately before closing the + * stream. + * + * Calling the {@link write} method after calling {@link end} will raise an error. + * + * ```js + * // Write 'hello, ' and then end with 'world!'. + * import fs from 'node:fs'; + * const file = fs.createWriteStream('example.txt'); + * file.write('hello, '); + * file.end('world!'); + * // Writing more now is not allowed! + * ``` + * @since v0.9.4 + * @param chunk Optional data to write. For streams not operating in object mode, `chunk` must be a {string}, {Buffer}, + * {TypedArray} or {DataView}. For object mode streams, `chunk` may be any JavaScript value other than `null`. + * @param encoding The encoding if `chunk` is a string + * @param callback Callback for when the stream is finished. + */ + end(cb?: () => void): this; + end(chunk: any, cb?: () => void): this; + end(chunk: any, encoding: BufferEncoding, cb?: () => void): this; + /** + * The `writable.cork()` method forces all written data to be buffered in memory. + * The buffered data will be flushed when either the {@link uncork} or {@link end} methods are called. + * + * The primary intent of `writable.cork()` is to accommodate a situation in which + * several small chunks are written to the stream in rapid succession. Instead of + * immediately forwarding them to the underlying destination, `writable.cork()` buffers all the chunks until `writable.uncork()` is called, which will pass them + * all to `writable._writev()`, if present. This prevents a head-of-line blocking + * situation where data is being buffered while waiting for the first small chunk + * to be processed. However, use of `writable.cork()` without implementing `writable._writev()` may have an adverse effect on throughput. + * + * See also: `writable.uncork()`, `writable._writev()`. + * @since v0.11.2 + */ + cork(): void; + /** + * The `writable.uncork()` method flushes all data buffered since {@link cork} was called. + * + * When using `writable.cork()` and `writable.uncork()` to manage the buffering + * of writes to a stream, defer calls to `writable.uncork()` using `process.nextTick()`. Doing so allows batching of all `writable.write()` calls that occur within a given Node.js event + * loop phase. + * + * ```js + * stream.cork(); + * stream.write('some '); + * stream.write('data '); + * process.nextTick(() => stream.uncork()); + * ``` + * + * If the `writable.cork()` method is called multiple times on a stream, the + * same number of calls to `writable.uncork()` must be called to flush the buffered + * data. + * + * ```js + * stream.cork(); + * stream.write('some '); + * stream.cork(); + * stream.write('data '); + * process.nextTick(() => { + * stream.uncork(); + * // The data will not be flushed until uncork() is called a second time. + * stream.uncork(); + * }); + * ``` + * + * See also: `writable.cork()`. + * @since v0.11.2 + */ + uncork(): void; + /** + * Destroy the stream. Optionally emit an `'error'` event, and emit a `'close'` event (unless `emitClose` is set to `false`). After this call, the writable + * stream has ended and subsequent calls to `write()` or `end()` will result in + * an `ERR_STREAM_DESTROYED` error. + * This is a destructive and immediate way to destroy a stream. Previous calls to `write()` may not have drained, and may trigger an `ERR_STREAM_DESTROYED` error. + * Use `end()` instead of destroy if data should flush before close, or wait for + * the `'drain'` event before destroying the stream. + * + * Once `destroy()` has been called any further calls will be a no-op and no + * further errors except from `_destroy()` may be emitted as `'error'`. + * + * Implementors should not override this method, + * but instead implement `writable._destroy()`. + * @since v8.0.0 + * @param error Optional, an error to emit with `'error'` event. + */ + destroy(error?: Error): this; + /** + * Event emitter + * The defined events on documents including: + * 1. close + * 2. drain + * 3. error + * 4. finish + * 5. pipe + * 6. unpipe + */ + addListener(event: "close", listener: () => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "finish", listener: () => void): this; + addListener(event: "pipe", listener: (src: Readable) => void): this; + addListener(event: "unpipe", listener: (src: Readable) => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "close"): boolean; + emit(event: "drain"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "finish"): boolean; + emit(event: "pipe", src: Readable): boolean; + emit(event: "unpipe", src: Readable): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "close", listener: () => void): this; + on(event: "drain", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "finish", listener: () => void): this; + on(event: "pipe", listener: (src: Readable) => void): this; + on(event: "unpipe", listener: (src: Readable) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "drain", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "finish", listener: () => void): this; + once(event: "pipe", listener: (src: Readable) => void): this; + once(event: "unpipe", listener: (src: Readable) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "finish", listener: () => void): this; + prependListener(event: "pipe", listener: (src: Readable) => void): this; + prependListener(event: "unpipe", listener: (src: Readable) => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "finish", listener: () => void): this; + prependOnceListener(event: "pipe", listener: (src: Readable) => void): this; + prependOnceListener(event: "unpipe", listener: (src: Readable) => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + removeListener(event: "close", listener: () => void): this; + removeListener(event: "drain", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: "finish", listener: () => void): this; + removeListener(event: "pipe", listener: (src: Readable) => void): this; + removeListener(event: "unpipe", listener: (src: Readable) => void): this; + removeListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + interface DuplexOptions extends ReadableOptions, WritableOptions { + allowHalfOpen?: boolean | undefined; + readableObjectMode?: boolean | undefined; + writableObjectMode?: boolean | undefined; + readableHighWaterMark?: number | undefined; + writableHighWaterMark?: number | undefined; + writableCorked?: number | undefined; + } + /** + * Duplex streams are streams that implement both the `Readable` and `Writable` interfaces. + * + * Examples of `Duplex` streams include: + * + * * `TCP sockets` + * * `zlib streams` + * * `crypto streams` + * @since v0.9.4 + */ + class Duplex extends Stream implements NodeJS.ReadWriteStream { + /** + * If `false` then the stream will automatically end the writable side when the + * readable side ends. Set initially by the `allowHalfOpen` constructor option, + * which defaults to `true`. + * + * This can be changed manually to change the half-open behavior of an existing + * `Duplex` stream instance, but must be changed before the `'end'` event is emitted. + * @since v0.9.4 + */ + allowHalfOpen: boolean; + constructor(opts?: DuplexOptions); + /** + * A utility method for creating duplex streams. + * + * - `Stream` converts writable stream into writable `Duplex` and readable stream + * to `Duplex`. + * - `Blob` converts into readable `Duplex`. + * - `string` converts into readable `Duplex`. + * - `ArrayBuffer` converts into readable `Duplex`. + * - `AsyncIterable` converts into a readable `Duplex`. Cannot yield `null`. + * - `AsyncGeneratorFunction` converts into a readable/writable transform + * `Duplex`. Must take a source `AsyncIterable` as first parameter. Cannot yield + * `null`. + * - `AsyncFunction` converts into a writable `Duplex`. Must return + * either `null` or `undefined` + * - `Object ({ writable, readable })` converts `readable` and + * `writable` into `Stream` and then combines them into `Duplex` where the + * `Duplex` will write to the `writable` and read from the `readable`. + * - `Promise` converts into readable `Duplex`. Value `null` is ignored. + * + * @since v16.8.0 + */ + static from( + src: + | Stream + | NodeBlob + | ArrayBuffer + | string + | Iterable + | AsyncIterable + | AsyncGeneratorFunction + | Promise + | Object, + ): Duplex; + /** + * A utility method for creating a web `ReadableStream` and `WritableStream` from a `Duplex`. + * @since v17.0.0 + */ + static toWeb(streamDuplex: Duplex): { + readable: streamWeb.ReadableStream; + writable: streamWeb.WritableStream; + }; + /** + * A utility method for creating a `Duplex` from a web `ReadableStream` and `WritableStream`. + * @since v17.0.0 + */ + static fromWeb( + duplexStream: { + readable: streamWeb.ReadableStream; + writable: streamWeb.WritableStream; + }, + options?: Pick< + DuplexOptions, + "allowHalfOpen" | "decodeStrings" | "encoding" | "highWaterMark" | "objectMode" | "signal" + >, + ): Duplex; + /** + * Event emitter + * The defined events on documents including: + * 1. close + * 2. data + * 3. drain + * 4. end + * 5. error + * 6. finish + * 7. pause + * 8. pipe + * 9. readable + * 10. resume + * 11. unpipe + */ + addListener(event: "close", listener: () => void): this; + addListener(event: "data", listener: (chunk: any) => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "end", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "finish", listener: () => void): this; + addListener(event: "pause", listener: () => void): this; + addListener(event: "pipe", listener: (src: Readable) => void): this; + addListener(event: "readable", listener: () => void): this; + addListener(event: "resume", listener: () => void): this; + addListener(event: "unpipe", listener: (src: Readable) => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: "close"): boolean; + emit(event: "data", chunk: any): boolean; + emit(event: "drain"): boolean; + emit(event: "end"): boolean; + emit(event: "error", err: Error): boolean; + emit(event: "finish"): boolean; + emit(event: "pause"): boolean; + emit(event: "pipe", src: Readable): boolean; + emit(event: "readable"): boolean; + emit(event: "resume"): boolean; + emit(event: "unpipe", src: Readable): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "close", listener: () => void): this; + on(event: "data", listener: (chunk: any) => void): this; + on(event: "drain", listener: () => void): this; + on(event: "end", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "finish", listener: () => void): this; + on(event: "pause", listener: () => void): this; + on(event: "pipe", listener: (src: Readable) => void): this; + on(event: "readable", listener: () => void): this; + on(event: "resume", listener: () => void): this; + on(event: "unpipe", listener: (src: Readable) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: "close", listener: () => void): this; + once(event: "data", listener: (chunk: any) => void): this; + once(event: "drain", listener: () => void): this; + once(event: "end", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "finish", listener: () => void): this; + once(event: "pause", listener: () => void): this; + once(event: "pipe", listener: (src: Readable) => void): this; + once(event: "readable", listener: () => void): this; + once(event: "resume", listener: () => void): this; + once(event: "unpipe", listener: (src: Readable) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "data", listener: (chunk: any) => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "end", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "finish", listener: () => void): this; + prependListener(event: "pause", listener: () => void): this; + prependListener(event: "pipe", listener: (src: Readable) => void): this; + prependListener(event: "readable", listener: () => void): this; + prependListener(event: "resume", listener: () => void): this; + prependListener(event: "unpipe", listener: (src: Readable) => void): this; + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "data", listener: (chunk: any) => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "end", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "finish", listener: () => void): this; + prependOnceListener(event: "pause", listener: () => void): this; + prependOnceListener(event: "pipe", listener: (src: Readable) => void): this; + prependOnceListener(event: "readable", listener: () => void): this; + prependOnceListener(event: "resume", listener: () => void): this; + prependOnceListener(event: "unpipe", listener: (src: Readable) => void): this; + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; + removeListener(event: "close", listener: () => void): this; + removeListener(event: "data", listener: (chunk: any) => void): this; + removeListener(event: "drain", listener: () => void): this; + removeListener(event: "end", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: "finish", listener: () => void): this; + removeListener(event: "pause", listener: () => void): this; + removeListener(event: "pipe", listener: (src: Readable) => void): this; + removeListener(event: "readable", listener: () => void): this; + removeListener(event: "resume", listener: () => void): this; + removeListener(event: "unpipe", listener: (src: Readable) => void): this; + removeListener(event: string | symbol, listener: (...args: any[]) => void): this; + } + interface Duplex extends Readable, Writable {} + /** + * The utility function `duplexPair` returns an Array with two items, + * each being a `Duplex` stream connected to the other side: + * + * ```js + * const [ sideA, sideB ] = duplexPair(); + * ``` + * + * Whatever is written to one stream is made readable on the other. It provides + * behavior analogous to a network connection, where the data written by the client + * becomes readable by the server, and vice-versa. + * + * The Duplex streams are symmetrical; one or the other may be used without any + * difference in behavior. + * @param options A value to pass to both {@link Duplex} constructors, + * to set options such as buffering. + * @since v22.6.0 + */ + function duplexPair(options?: DuplexOptions): [Duplex, Duplex]; + type TransformCallback = (error?: Error | null, data?: any) => void; + interface TransformOptions extends DuplexOptions { + transform?: + | ((this: T, chunk: any, encoding: BufferEncoding, callback: TransformCallback) => void) + | undefined; + flush?: ((this: T, callback: TransformCallback) => void) | undefined; + } + /** + * Transform streams are `Duplex` streams where the output is in some way + * related to the input. Like all `Duplex` streams, `Transform` streams + * implement both the `Readable` and `Writable` interfaces. + * + * Examples of `Transform` streams include: + * + * * `zlib streams` + * * `crypto streams` + * @since v0.9.4 + */ + class Transform extends Duplex { + constructor(opts?: TransformOptions); + _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void; + _flush(callback: TransformCallback): void; + } + /** + * The `stream.PassThrough` class is a trivial implementation of a `Transform` stream that simply passes the input bytes across to the output. Its purpose is + * primarily for examples and testing, but there are some use cases where `stream.PassThrough` is useful as a building block for novel sorts of streams. + */ + class PassThrough extends Transform {} + /** + * A stream to attach a signal to. + * + * Attaches an AbortSignal to a readable or writeable stream. This lets code + * control stream destruction using an `AbortController`. + * + * Calling `abort` on the `AbortController` corresponding to the passed `AbortSignal` will behave the same way as calling `.destroy(new AbortError())` on the + * stream, and `controller.error(new AbortError())` for webstreams. + * + * ```js + * import fs from 'node:fs'; + * + * const controller = new AbortController(); + * const read = addAbortSignal( + * controller.signal, + * fs.createReadStream(('object.json')), + * ); + * // Later, abort the operation closing the stream + * controller.abort(); + * ``` + * + * Or using an `AbortSignal` with a readable stream as an async iterable: + * + * ```js + * const controller = new AbortController(); + * setTimeout(() => controller.abort(), 10_000); // set a timeout + * const stream = addAbortSignal( + * controller.signal, + * fs.createReadStream(('object.json')), + * ); + * (async () => { + * try { + * for await (const chunk of stream) { + * await process(chunk); + * } + * } catch (e) { + * if (e.name === 'AbortError') { + * // The operation was cancelled + * } else { + * throw e; + * } + * } + * })(); + * ``` + * + * Or using an `AbortSignal` with a ReadableStream: + * + * ```js + * const controller = new AbortController(); + * const rs = new ReadableStream({ + * start(controller) { + * controller.enqueue('hello'); + * controller.enqueue('world'); + * controller.close(); + * }, + * }); + * + * addAbortSignal(controller.signal, rs); + * + * finished(rs, (err) => { + * if (err) { + * if (err.name === 'AbortError') { + * // The operation was cancelled + * } + * } + * }); + * + * const reader = rs.getReader(); + * + * reader.read().then(({ value, done }) => { + * console.log(value); // hello + * console.log(done); // false + * controller.abort(); + * }); + * ``` + * @since v15.4.0 + * @param signal A signal representing possible cancellation + * @param stream A stream to attach a signal to. + */ + function addAbortSignal(signal: AbortSignal, stream: T): T; + /** + * Returns the default highWaterMark used by streams. + * Defaults to `65536` (64 KiB), or `16` for `objectMode`. + * @since v19.9.0 + */ + function getDefaultHighWaterMark(objectMode: boolean): number; + /** + * Sets the default highWaterMark used by streams. + * @since v19.9.0 + * @param value highWaterMark value + */ + function setDefaultHighWaterMark(objectMode: boolean, value: number): void; + interface FinishedOptions extends Abortable { + error?: boolean | undefined; + readable?: boolean | undefined; + writable?: boolean | undefined; + } + /** + * A readable and/or writable stream/webstream. + * + * A function to get notified when a stream is no longer readable, writable + * or has experienced an error or a premature close event. + * + * ```js + * import { finished } from 'node:stream'; + * import fs from 'node:fs'; + * + * const rs = fs.createReadStream('archive.tar'); + * + * finished(rs, (err) => { + * if (err) { + * console.error('Stream failed.', err); + * } else { + * console.log('Stream is done reading.'); + * } + * }); + * + * rs.resume(); // Drain the stream. + * ``` + * + * Especially useful in error handling scenarios where a stream is destroyed + * prematurely (like an aborted HTTP request), and will not emit `'end'` or `'finish'`. + * + * The `finished` API provides [`promise version`](https://nodejs.org/docs/latest-v22.x/api/stream.html#streamfinishedstream-options). + * + * `stream.finished()` leaves dangling event listeners (in particular `'error'`, `'end'`, `'finish'` and `'close'`) after `callback` has been + * invoked. The reason for this is so that unexpected `'error'` events (due to + * incorrect stream implementations) do not cause unexpected crashes. + * If this is unwanted behavior then the returned cleanup function needs to be + * invoked in the callback: + * + * ```js + * const cleanup = finished(rs, (err) => { + * cleanup(); + * // ... + * }); + * ``` + * @since v10.0.0 + * @param stream A readable and/or writable stream. + * @param callback A callback function that takes an optional error argument. + * @returns A cleanup function which removes all registered listeners. + */ + function finished( + stream: NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream, + options: FinishedOptions, + callback: (err?: NodeJS.ErrnoException | null) => void, + ): () => void; + function finished( + stream: NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream, + callback: (err?: NodeJS.ErrnoException | null) => void, + ): () => void; + namespace finished { + function __promisify__( + stream: NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream, + options?: FinishedOptions, + ): Promise; + } + type PipelineSourceFunction = () => Iterable | AsyncIterable; + type PipelineSource = Iterable | AsyncIterable | NodeJS.ReadableStream | PipelineSourceFunction; + type PipelineTransform, U> = + | NodeJS.ReadWriteStream + | (( + source: S extends (...args: any[]) => Iterable | AsyncIterable ? AsyncIterable + : S, + ) => AsyncIterable); + type PipelineTransformSource = PipelineSource | PipelineTransform; + type PipelineDestinationIterableFunction = (source: AsyncIterable) => AsyncIterable; + type PipelineDestinationPromiseFunction = (source: AsyncIterable) => Promise

; + type PipelineDestination, P> = S extends + PipelineTransformSource ? + | NodeJS.WritableStream + | PipelineDestinationIterableFunction + | PipelineDestinationPromiseFunction + : never; + type PipelineCallback> = S extends + PipelineDestinationPromiseFunction ? (err: NodeJS.ErrnoException | null, value: P) => void + : (err: NodeJS.ErrnoException | null) => void; + type PipelinePromise> = S extends + PipelineDestinationPromiseFunction ? Promise

: Promise; + interface PipelineOptions { + signal?: AbortSignal | undefined; + end?: boolean | undefined; + } + /** + * A module method to pipe between streams and generators forwarding errors and + * properly cleaning up and provide a callback when the pipeline is complete. + * + * ```js + * import { pipeline } from 'node:stream'; + * import fs from 'node:fs'; + * import zlib from 'node:zlib'; + * + * // Use the pipeline API to easily pipe a series of streams + * // together and get notified when the pipeline is fully done. + * + * // A pipeline to gzip a potentially huge tar file efficiently: + * + * pipeline( + * fs.createReadStream('archive.tar'), + * zlib.createGzip(), + * fs.createWriteStream('archive.tar.gz'), + * (err) => { + * if (err) { + * console.error('Pipeline failed.', err); + * } else { + * console.log('Pipeline succeeded.'); + * } + * }, + * ); + * ``` + * + * The `pipeline` API provides a [`promise version`](https://nodejs.org/docs/latest-v22.x/api/stream.html#streampipelinesource-transforms-destination-options). + * + * `stream.pipeline()` will call `stream.destroy(err)` on all streams except: + * + * * `Readable` streams which have emitted `'end'` or `'close'`. + * * `Writable` streams which have emitted `'finish'` or `'close'`. + * + * `stream.pipeline()` leaves dangling event listeners on the streams + * after the `callback` has been invoked. In the case of reuse of streams after + * failure, this can cause event listener leaks and swallowed errors. If the last + * stream is readable, dangling event listeners will be removed so that the last + * stream can be consumed later. + * + * `stream.pipeline()` closes all the streams when an error is raised. + * The `IncomingRequest` usage with `pipeline` could lead to an unexpected behavior + * once it would destroy the socket without sending the expected response. + * See the example below: + * + * ```js + * import fs from 'node:fs'; + * import http from 'node:http'; + * import { pipeline } from 'node:stream'; + * + * const server = http.createServer((req, res) => { + * const fileStream = fs.createReadStream('./fileNotExist.txt'); + * pipeline(fileStream, res, (err) => { + * if (err) { + * console.log(err); // No such file + * // this message can't be sent once `pipeline` already destroyed the socket + * return res.end('error!!!'); + * } + * }); + * }); + * ``` + * @since v10.0.0 + * @param callback Called when the pipeline is fully done. + */ + function pipeline, B extends PipelineDestination>( + source: A, + destination: B, + callback: PipelineCallback, + ): B extends NodeJS.WritableStream ? B : NodeJS.WritableStream; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + destination: B, + callback: PipelineCallback, + ): B extends NodeJS.WritableStream ? B : NodeJS.WritableStream; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + destination: B, + callback: PipelineCallback, + ): B extends NodeJS.WritableStream ? B : NodeJS.WritableStream; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + T3 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + transform3: T3, + destination: B, + callback: PipelineCallback, + ): B extends NodeJS.WritableStream ? B : NodeJS.WritableStream; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + T3 extends PipelineTransform, + T4 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + transform3: T3, + transform4: T4, + destination: B, + callback: PipelineCallback, + ): B extends NodeJS.WritableStream ? B : NodeJS.WritableStream; + function pipeline( + streams: ReadonlyArray, + callback: (err: NodeJS.ErrnoException | null) => void, + ): NodeJS.WritableStream; + function pipeline( + stream1: NodeJS.ReadableStream, + stream2: NodeJS.ReadWriteStream | NodeJS.WritableStream, + ...streams: Array< + NodeJS.ReadWriteStream | NodeJS.WritableStream | ((err: NodeJS.ErrnoException | null) => void) + > + ): NodeJS.WritableStream; + namespace pipeline { + function __promisify__, B extends PipelineDestination>( + source: A, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function __promisify__< + A extends PipelineSource, + T1 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function __promisify__< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function __promisify__< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + T3 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + transform3: T3, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function __promisify__< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + T3 extends PipelineTransform, + T4 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + transform3: T3, + transform4: T4, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function __promisify__( + streams: ReadonlyArray, + options?: PipelineOptions, + ): Promise; + function __promisify__( + stream1: NodeJS.ReadableStream, + stream2: NodeJS.ReadWriteStream | NodeJS.WritableStream, + ...streams: Array + ): Promise; + } + // TODO: this interface never existed; remove in next major + interface Pipe { + close(): void; + hasRef(): boolean; + ref(): void; + unref(): void; + } + /** + * Returns whether the stream has encountered an error. + * @since v17.3.0, v16.14.0 + */ + function isErrored(stream: Readable | Writable | NodeJS.ReadableStream | NodeJS.WritableStream): boolean; + /** + * Returns whether the stream is readable. + * @since v17.4.0, v16.14.0 + */ + function isReadable(stream: Readable | NodeJS.ReadableStream): boolean; + } + export = Stream; +} +declare module "node:stream" { + import stream = require("stream"); + export = stream; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/consumers.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/consumers.d.ts new file mode 100644 index 00000000..05db0257 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/consumers.d.ts @@ -0,0 +1,38 @@ +/** + * The utility consumer functions provide common options for consuming + * streams. + * @since v16.7.0 + */ +declare module "stream/consumers" { + import { Blob as NodeBlob, NonSharedBuffer } from "node:buffer"; + import { ReadableStream as WebReadableStream } from "node:stream/web"; + /** + * @since v16.7.0 + * @returns Fulfills with an `ArrayBuffer` containing the full contents of the stream. + */ + function arrayBuffer(stream: WebReadableStream | NodeJS.ReadableStream | AsyncIterable): Promise; + /** + * @since v16.7.0 + * @returns Fulfills with a `Blob` containing the full contents of the stream. + */ + function blob(stream: WebReadableStream | NodeJS.ReadableStream | AsyncIterable): Promise; + /** + * @since v16.7.0 + * @returns Fulfills with a `Buffer` containing the full contents of the stream. + */ + function buffer(stream: WebReadableStream | NodeJS.ReadableStream | AsyncIterable): Promise; + /** + * @since v16.7.0 + * @returns Fulfills with the contents of the stream parsed as a + * UTF-8 encoded string that is then passed through `JSON.parse()`. + */ + function json(stream: WebReadableStream | NodeJS.ReadableStream | AsyncIterable): Promise; + /** + * @since v16.7.0 + * @returns Fulfills with the contents of the stream parsed as a UTF-8 encoded string. + */ + function text(stream: WebReadableStream | NodeJS.ReadableStream | AsyncIterable): Promise; +} +declare module "node:stream/consumers" { + export * from "stream/consumers"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/promises.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/promises.d.ts new file mode 100644 index 00000000..d54c14c6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/promises.d.ts @@ -0,0 +1,90 @@ +declare module "stream/promises" { + import { + FinishedOptions as _FinishedOptions, + PipelineDestination, + PipelineOptions, + PipelinePromise, + PipelineSource, + PipelineTransform, + } from "node:stream"; + interface FinishedOptions extends _FinishedOptions { + /** + * If true, removes the listeners registered by this function before the promise is fulfilled. + * @default false + */ + cleanup?: boolean | undefined; + } + function finished( + stream: NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream, + options?: FinishedOptions, + ): Promise; + function pipeline, B extends PipelineDestination>( + source: A, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + T3 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + transform3: T3, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function pipeline< + A extends PipelineSource, + T1 extends PipelineTransform, + T2 extends PipelineTransform, + T3 extends PipelineTransform, + T4 extends PipelineTransform, + B extends PipelineDestination, + >( + source: A, + transform1: T1, + transform2: T2, + transform3: T3, + transform4: T4, + destination: B, + options?: PipelineOptions, + ): PipelinePromise; + function pipeline( + streams: ReadonlyArray, + options?: PipelineOptions, + ): Promise; + function pipeline( + stream1: NodeJS.ReadableStream, + stream2: NodeJS.ReadWriteStream | NodeJS.WritableStream, + ...streams: Array + ): Promise; +} +declare module "node:stream/promises" { + export * from "stream/promises"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/web.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/web.d.ts new file mode 100644 index 00000000..8d348a39 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/stream/web.d.ts @@ -0,0 +1,622 @@ +type _ByteLengthQueuingStrategy = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").ByteLengthQueuingStrategy; +type _CompressionStream = typeof globalThis extends { onmessage: any; ReportingObserver: any } ? {} + : import("stream/web").CompressionStream; +type _CountQueuingStrategy = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").CountQueuingStrategy; +type _DecompressionStream = typeof globalThis extends { onmessage: any; ReportingObserver: any } ? {} + : import("stream/web").DecompressionStream; +type _QueuingStrategy = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").QueuingStrategy; +type _ReadableByteStreamController = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").ReadableByteStreamController; +type _ReadableStream = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").ReadableStream; +type _ReadableStreamBYOBReader = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").ReadableStreamBYOBReader; +type _ReadableStreamBYOBRequest = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").ReadableStreamBYOBRequest; +type _ReadableStreamDefaultController = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").ReadableStreamDefaultController; +type _ReadableStreamDefaultReader = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").ReadableStreamDefaultReader; +type _TextDecoderStream = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").TextDecoderStream; +type _TextEncoderStream = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").TextEncoderStream; +type _TransformStream = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").TransformStream; +type _TransformStreamDefaultController = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").TransformStreamDefaultController; +type _WritableStream = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").WritableStream; +type _WritableStreamDefaultController = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").WritableStreamDefaultController; +type _WritableStreamDefaultWriter = typeof globalThis extends { onmessage: any } ? {} + : import("stream/web").WritableStreamDefaultWriter; + +declare module "stream/web" { + // stub module, pending copy&paste from .d.ts or manual impl + // copy from lib.dom.d.ts + interface ReadableWritablePair { + readable: ReadableStream; + /** + * Provides a convenient, chainable way of piping this readable stream + * through a transform stream (or any other { writable, readable } + * pair). It simply pipes the stream into the writable side of the + * supplied pair, and returns the readable side for further use. + * + * Piping a stream will lock it for the duration of the pipe, preventing + * any other consumer from acquiring a reader. + */ + writable: WritableStream; + } + interface StreamPipeOptions { + preventAbort?: boolean; + preventCancel?: boolean; + /** + * Pipes this readable stream to a given writable stream destination. + * The way in which the piping process behaves under various error + * conditions can be customized with a number of passed options. It + * returns a promise that fulfills when the piping process completes + * successfully, or rejects if any errors were encountered. + * + * Piping a stream will lock it for the duration of the pipe, preventing + * any other consumer from acquiring a reader. + * + * Errors and closures of the source and destination streams propagate + * as follows: + * + * An error in this source readable stream will abort destination, + * unless preventAbort is truthy. The returned promise will be rejected + * with the source's error, or with any error that occurs during + * aborting the destination. + * + * An error in destination will cancel this source readable stream, + * unless preventCancel is truthy. The returned promise will be rejected + * with the destination's error, or with any error that occurs during + * canceling the source. + * + * When this source readable stream closes, destination will be closed, + * unless preventClose is truthy. The returned promise will be fulfilled + * once this process completes, unless an error is encountered while + * closing the destination, in which case it will be rejected with that + * error. + * + * If destination starts out closed or closing, this source readable + * stream will be canceled, unless preventCancel is true. The returned + * promise will be rejected with an error indicating piping to a closed + * stream failed, or with any error that occurs during canceling the + * source. + * + * The signal option can be set to an AbortSignal to allow aborting an + * ongoing pipe operation via the corresponding AbortController. In this + * case, this source readable stream will be canceled, and destination + * aborted, unless the respective options preventCancel or preventAbort + * are set. + */ + preventClose?: boolean; + signal?: AbortSignal; + } + interface ReadableStreamGenericReader { + readonly closed: Promise; + cancel(reason?: any): Promise; + } + type ReadableStreamController = ReadableStreamDefaultController; + interface ReadableStreamReadValueResult { + done: false; + value: T; + } + interface ReadableStreamReadDoneResult { + done: true; + value?: T; + } + type ReadableStreamReadResult = ReadableStreamReadValueResult | ReadableStreamReadDoneResult; + interface ReadableByteStreamControllerCallback { + (controller: ReadableByteStreamController): void | PromiseLike; + } + interface UnderlyingSinkAbortCallback { + (reason?: any): void | PromiseLike; + } + interface UnderlyingSinkCloseCallback { + (): void | PromiseLike; + } + interface UnderlyingSinkStartCallback { + (controller: WritableStreamDefaultController): any; + } + interface UnderlyingSinkWriteCallback { + (chunk: W, controller: WritableStreamDefaultController): void | PromiseLike; + } + interface UnderlyingSourceCancelCallback { + (reason?: any): void | PromiseLike; + } + interface UnderlyingSourcePullCallback { + (controller: ReadableStreamController): void | PromiseLike; + } + interface UnderlyingSourceStartCallback { + (controller: ReadableStreamController): any; + } + interface TransformerFlushCallback { + (controller: TransformStreamDefaultController): void | PromiseLike; + } + interface TransformerStartCallback { + (controller: TransformStreamDefaultController): any; + } + interface TransformerTransformCallback { + (chunk: I, controller: TransformStreamDefaultController): void | PromiseLike; + } + interface TransformerCancelCallback { + (reason: any): void | PromiseLike; + } + interface UnderlyingByteSource { + autoAllocateChunkSize?: number; + cancel?: ReadableStreamErrorCallback; + pull?: ReadableByteStreamControllerCallback; + start?: ReadableByteStreamControllerCallback; + type: "bytes"; + } + interface UnderlyingSource { + cancel?: UnderlyingSourceCancelCallback; + pull?: UnderlyingSourcePullCallback; + start?: UnderlyingSourceStartCallback; + type?: undefined; + } + interface UnderlyingSink { + abort?: UnderlyingSinkAbortCallback; + close?: UnderlyingSinkCloseCallback; + start?: UnderlyingSinkStartCallback; + type?: undefined; + write?: UnderlyingSinkWriteCallback; + } + interface ReadableStreamErrorCallback { + (reason: any): void | PromiseLike; + } + interface ReadableStreamAsyncIterator extends NodeJS.AsyncIterator { + [Symbol.asyncIterator](): ReadableStreamAsyncIterator; + } + /** This Streams API interface represents a readable stream of byte data. */ + interface ReadableStream { + readonly locked: boolean; + cancel(reason?: any): Promise; + getReader(options: { mode: "byob" }): ReadableStreamBYOBReader; + getReader(): ReadableStreamDefaultReader; + getReader(options?: ReadableStreamGetReaderOptions): ReadableStreamReader; + pipeThrough(transform: ReadableWritablePair, options?: StreamPipeOptions): ReadableStream; + pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise; + tee(): [ReadableStream, ReadableStream]; + values(options?: { preventCancel?: boolean }): ReadableStreamAsyncIterator; + [Symbol.asyncIterator](): ReadableStreamAsyncIterator; + } + const ReadableStream: { + prototype: ReadableStream; + from(iterable: Iterable | AsyncIterable): ReadableStream; + new(underlyingSource: UnderlyingByteSource, strategy?: QueuingStrategy): ReadableStream; + new(underlyingSource?: UnderlyingSource, strategy?: QueuingStrategy): ReadableStream; + }; + type ReadableStreamReaderMode = "byob"; + interface ReadableStreamGetReaderOptions { + /** + * Creates a ReadableStreamBYOBReader and locks the stream to the new reader. + * + * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle "bring your own buffer" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation. + */ + mode?: ReadableStreamReaderMode; + } + type ReadableStreamReader = ReadableStreamDefaultReader | ReadableStreamBYOBReader; + interface ReadableStreamDefaultReader extends ReadableStreamGenericReader { + read(): Promise>; + releaseLock(): void; + } + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader) */ + interface ReadableStreamBYOBReader extends ReadableStreamGenericReader { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read) */ + read( + view: T, + options?: { + min?: number; + }, + ): Promise>; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock) */ + releaseLock(): void; + } + const ReadableStreamDefaultReader: { + prototype: ReadableStreamDefaultReader; + new(stream: ReadableStream): ReadableStreamDefaultReader; + }; + const ReadableStreamBYOBReader: { + prototype: ReadableStreamBYOBReader; + new(stream: ReadableStream): ReadableStreamBYOBReader; + }; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest) */ + interface ReadableStreamBYOBRequest { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/view) */ + readonly view: ArrayBufferView | null; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respond) */ + respond(bytesWritten: number): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respondWithNewView) */ + respondWithNewView(view: ArrayBufferView): void; + } + const ReadableStreamBYOBRequest: { + prototype: ReadableStreamBYOBRequest; + new(): ReadableStreamBYOBRequest; + }; + interface ReadableByteStreamController { + readonly byobRequest: ReadableStreamBYOBRequest | null; + readonly desiredSize: number | null; + close(): void; + enqueue(chunk: ArrayBufferView): void; + error(error?: any): void; + } + const ReadableByteStreamController: { + prototype: ReadableByteStreamController; + new(): ReadableByteStreamController; + }; + interface ReadableStreamDefaultController { + readonly desiredSize: number | null; + close(): void; + enqueue(chunk?: R): void; + error(e?: any): void; + } + const ReadableStreamDefaultController: { + prototype: ReadableStreamDefaultController; + new(): ReadableStreamDefaultController; + }; + interface Transformer { + flush?: TransformerFlushCallback; + readableType?: undefined; + start?: TransformerStartCallback; + transform?: TransformerTransformCallback; + cancel?: TransformerCancelCallback; + writableType?: undefined; + } + interface TransformStream { + readonly readable: ReadableStream; + readonly writable: WritableStream; + } + const TransformStream: { + prototype: TransformStream; + new( + transformer?: Transformer, + writableStrategy?: QueuingStrategy, + readableStrategy?: QueuingStrategy, + ): TransformStream; + }; + interface TransformStreamDefaultController { + readonly desiredSize: number | null; + enqueue(chunk?: O): void; + error(reason?: any): void; + terminate(): void; + } + const TransformStreamDefaultController: { + prototype: TransformStreamDefaultController; + new(): TransformStreamDefaultController; + }; + /** + * This Streams API interface provides a standard abstraction for writing + * streaming data to a destination, known as a sink. This object comes with + * built-in back pressure and queuing. + */ + interface WritableStream { + readonly locked: boolean; + abort(reason?: any): Promise; + close(): Promise; + getWriter(): WritableStreamDefaultWriter; + } + const WritableStream: { + prototype: WritableStream; + new(underlyingSink?: UnderlyingSink, strategy?: QueuingStrategy): WritableStream; + }; + /** + * This Streams API interface is the object returned by + * WritableStream.getWriter() and once created locks the < writer to the + * WritableStream ensuring that no other streams can write to the underlying + * sink. + */ + interface WritableStreamDefaultWriter { + readonly closed: Promise; + readonly desiredSize: number | null; + readonly ready: Promise; + abort(reason?: any): Promise; + close(): Promise; + releaseLock(): void; + write(chunk?: W): Promise; + } + const WritableStreamDefaultWriter: { + prototype: WritableStreamDefaultWriter; + new(stream: WritableStream): WritableStreamDefaultWriter; + }; + /** + * This Streams API interface represents a controller allowing control of a + * WritableStream's state. When constructing a WritableStream, the + * underlying sink is given a corresponding WritableStreamDefaultController + * instance to manipulate. + */ + interface WritableStreamDefaultController { + error(e?: any): void; + } + const WritableStreamDefaultController: { + prototype: WritableStreamDefaultController; + new(): WritableStreamDefaultController; + }; + interface QueuingStrategy { + highWaterMark?: number; + size?: QueuingStrategySize; + } + interface QueuingStrategySize { + (chunk?: T): number; + } + interface QueuingStrategyInit { + /** + * Creates a new ByteLengthQueuingStrategy with the provided high water + * mark. + * + * Note that the provided high water mark will not be validated ahead of + * time. Instead, if it is negative, NaN, or not a number, the resulting + * ByteLengthQueuingStrategy will cause the corresponding stream + * constructor to throw. + */ + highWaterMark: number; + } + /** + * This Streams API interface provides a built-in byte length queuing + * strategy that can be used when constructing streams. + */ + interface ByteLengthQueuingStrategy extends QueuingStrategy { + readonly highWaterMark: number; + readonly size: QueuingStrategySize; + } + const ByteLengthQueuingStrategy: { + prototype: ByteLengthQueuingStrategy; + new(init: QueuingStrategyInit): ByteLengthQueuingStrategy; + }; + /** + * This Streams API interface provides a built-in byte length queuing + * strategy that can be used when constructing streams. + */ + interface CountQueuingStrategy extends QueuingStrategy { + readonly highWaterMark: number; + readonly size: QueuingStrategySize; + } + const CountQueuingStrategy: { + prototype: CountQueuingStrategy; + new(init: QueuingStrategyInit): CountQueuingStrategy; + }; + interface TextEncoderStream { + /** Returns "utf-8". */ + readonly encoding: "utf-8"; + readonly readable: ReadableStream; + readonly writable: WritableStream; + readonly [Symbol.toStringTag]: string; + } + const TextEncoderStream: { + prototype: TextEncoderStream; + new(): TextEncoderStream; + }; + interface TextDecoderOptions { + fatal?: boolean; + ignoreBOM?: boolean; + } + type BufferSource = ArrayBufferView | ArrayBuffer; + interface TextDecoderStream { + /** Returns encoding's name, lower cased. */ + readonly encoding: string; + /** Returns `true` if error mode is "fatal", and `false` otherwise. */ + readonly fatal: boolean; + /** Returns `true` if ignore BOM flag is set, and `false` otherwise. */ + readonly ignoreBOM: boolean; + readonly readable: ReadableStream; + readonly writable: WritableStream; + readonly [Symbol.toStringTag]: string; + } + const TextDecoderStream: { + prototype: TextDecoderStream; + new(encoding?: string, options?: TextDecoderOptions): TextDecoderStream; + }; + interface CompressionStream { + readonly readable: ReadableStream; + readonly writable: WritableStream; + } + const CompressionStream: { + prototype: CompressionStream; + new(format: "deflate" | "deflate-raw" | "gzip"): CompressionStream; + }; + interface DecompressionStream { + readonly writable: WritableStream; + readonly readable: ReadableStream; + } + const DecompressionStream: { + prototype: DecompressionStream; + new(format: "deflate" | "deflate-raw" | "gzip"): DecompressionStream; + }; + + global { + interface ByteLengthQueuingStrategy extends _ByteLengthQueuingStrategy {} + /** + * `ByteLengthQueuingStrategy` class is a global reference for `import { ByteLengthQueuingStrategy } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-bytelengthqueuingstrategy + * @since v18.0.0 + */ + var ByteLengthQueuingStrategy: typeof globalThis extends { onmessage: any; ByteLengthQueuingStrategy: infer T } + ? T + : typeof import("stream/web").ByteLengthQueuingStrategy; + + interface CompressionStream extends _CompressionStream {} + /** + * `CompressionStream` class is a global reference for `import { CompressionStream } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-compressionstream + * @since v18.0.0 + */ + var CompressionStream: typeof globalThis extends { + onmessage: any; + // CompressionStream, DecompressionStream and ReportingObserver was introduced in the same commit. + // If ReportingObserver check is removed, the type here will form a circular reference in TS5.0+lib.dom.d.ts + ReportingObserver: any; + CompressionStream: infer T; + } ? T + // TS 4.8, 4.9, 5.0 + : typeof globalThis extends { onmessage: any; TransformStream: { prototype: infer T } } ? { + prototype: T; + new(format: "deflate" | "deflate-raw" | "gzip"): T; + } + : typeof import("stream/web").CompressionStream; + + interface CountQueuingStrategy extends _CountQueuingStrategy {} + /** + * `CountQueuingStrategy` class is a global reference for `import { CountQueuingStrategy } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-countqueuingstrategy + * @since v18.0.0 + */ + var CountQueuingStrategy: typeof globalThis extends { onmessage: any; CountQueuingStrategy: infer T } ? T + : typeof import("stream/web").CountQueuingStrategy; + + interface DecompressionStream extends _DecompressionStream {} + /** + * `DecompressionStream` class is a global reference for `import { DecompressionStream } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-decompressionstream + * @since v18.0.0 + */ + var DecompressionStream: typeof globalThis extends { + onmessage: any; + // CompressionStream, DecompressionStream and ReportingObserver was introduced in the same commit. + // If ReportingObserver check is removed, the type here will form a circular reference in TS5.0+lib.dom.d.ts + ReportingObserver: any; + DecompressionStream: infer T extends object; + } ? T + // TS 4.8, 4.9, 5.0 + : typeof globalThis extends { onmessage: any; TransformStream: { prototype: infer T } } ? { + prototype: T; + new(format: "deflate" | "deflate-raw" | "gzip"): T; + } + : typeof import("stream/web").DecompressionStream; + + interface QueuingStrategy extends _QueuingStrategy {} + + interface ReadableByteStreamController extends _ReadableByteStreamController {} + /** + * `ReadableByteStreamController` class is a global reference for `import { ReadableByteStreamController } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-readablebytestreamcontroller + * @since v18.0.0 + */ + var ReadableByteStreamController: typeof globalThis extends + { onmessage: any; ReadableByteStreamController: infer T } ? T + : typeof import("stream/web").ReadableByteStreamController; + + interface ReadableStream extends _ReadableStream {} + /** + * `ReadableStream` class is a global reference for `import { ReadableStream } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-readablestream + * @since v18.0.0 + */ + var ReadableStream: typeof globalThis extends { onmessage: any; ReadableStream: infer T } ? T + : typeof import("stream/web").ReadableStream; + + interface ReadableStreamBYOBReader extends _ReadableStreamBYOBReader {} + /** + * `ReadableStreamBYOBReader` class is a global reference for `import { ReadableStreamBYOBReader } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-readablestreambyobreader + * @since v18.0.0 + */ + var ReadableStreamBYOBReader: typeof globalThis extends { onmessage: any; ReadableStreamBYOBReader: infer T } + ? T + : typeof import("stream/web").ReadableStreamBYOBReader; + + interface ReadableStreamBYOBRequest extends _ReadableStreamBYOBRequest {} + /** + * `ReadableStreamBYOBRequest` class is a global reference for `import { ReadableStreamBYOBRequest } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-readablestreambyobrequest + * @since v18.0.0 + */ + var ReadableStreamBYOBRequest: typeof globalThis extends { onmessage: any; ReadableStreamBYOBRequest: infer T } + ? T + : typeof import("stream/web").ReadableStreamBYOBRequest; + + interface ReadableStreamDefaultController extends _ReadableStreamDefaultController {} + /** + * `ReadableStreamDefaultController` class is a global reference for `import { ReadableStreamDefaultController } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-readablestreamdefaultcontroller + * @since v18.0.0 + */ + var ReadableStreamDefaultController: typeof globalThis extends + { onmessage: any; ReadableStreamDefaultController: infer T } ? T + : typeof import("stream/web").ReadableStreamDefaultController; + + interface ReadableStreamDefaultReader extends _ReadableStreamDefaultReader {} + /** + * `ReadableStreamDefaultReader` class is a global reference for `import { ReadableStreamDefaultReader } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-readablestreamdefaultreader + * @since v18.0.0 + */ + var ReadableStreamDefaultReader: typeof globalThis extends + { onmessage: any; ReadableStreamDefaultReader: infer T } ? T + : typeof import("stream/web").ReadableStreamDefaultReader; + + interface TextDecoderStream extends _TextDecoderStream {} + /** + * `TextDecoderStream` class is a global reference for `import { TextDecoderStream } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-textdecoderstream + * @since v18.0.0 + */ + var TextDecoderStream: typeof globalThis extends { onmessage: any; TextDecoderStream: infer T } ? T + : typeof import("stream/web").TextDecoderStream; + + interface TextEncoderStream extends _TextEncoderStream {} + /** + * `TextEncoderStream` class is a global reference for `import { TextEncoderStream } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-textencoderstream + * @since v18.0.0 + */ + var TextEncoderStream: typeof globalThis extends { onmessage: any; TextEncoderStream: infer T } ? T + : typeof import("stream/web").TextEncoderStream; + + interface TransformStream extends _TransformStream {} + /** + * `TransformStream` class is a global reference for `import { TransformStream } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-transformstream + * @since v18.0.0 + */ + var TransformStream: typeof globalThis extends { onmessage: any; TransformStream: infer T } ? T + : typeof import("stream/web").TransformStream; + + interface TransformStreamDefaultController extends _TransformStreamDefaultController {} + /** + * `TransformStreamDefaultController` class is a global reference for `import { TransformStreamDefaultController } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-transformstreamdefaultcontroller + * @since v18.0.0 + */ + var TransformStreamDefaultController: typeof globalThis extends + { onmessage: any; TransformStreamDefaultController: infer T } ? T + : typeof import("stream/web").TransformStreamDefaultController; + + interface WritableStream extends _WritableStream {} + /** + * `WritableStream` class is a global reference for `import { WritableStream } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-writablestream + * @since v18.0.0 + */ + var WritableStream: typeof globalThis extends { onmessage: any; WritableStream: infer T } ? T + : typeof import("stream/web").WritableStream; + + interface WritableStreamDefaultController extends _WritableStreamDefaultController {} + /** + * `WritableStreamDefaultController` class is a global reference for `import { WritableStreamDefaultController } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-writablestreamdefaultcontroller + * @since v18.0.0 + */ + var WritableStreamDefaultController: typeof globalThis extends + { onmessage: any; WritableStreamDefaultController: infer T } ? T + : typeof import("stream/web").WritableStreamDefaultController; + + interface WritableStreamDefaultWriter extends _WritableStreamDefaultWriter {} + /** + * `WritableStreamDefaultWriter` class is a global reference for `import { WritableStreamDefaultWriter } from 'node:stream/web'`. + * https://nodejs.org/api/globals.html#class-writablestreamdefaultwriter + * @since v18.0.0 + */ + var WritableStreamDefaultWriter: typeof globalThis extends + { onmessage: any; WritableStreamDefaultWriter: infer T } ? T + : typeof import("stream/web").WritableStreamDefaultWriter; + } +} +declare module "node:stream/web" { + export * from "stream/web"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/string_decoder.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/string_decoder.d.ts new file mode 100644 index 00000000..d8b9be86 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/string_decoder.d.ts @@ -0,0 +1,67 @@ +/** + * The `node:string_decoder` module provides an API for decoding `Buffer` objects + * into strings in a manner that preserves encoded multi-byte UTF-8 and UTF-16 + * characters. It can be accessed using: + * + * ```js + * import { StringDecoder } from 'node:string_decoder'; + * ``` + * + * The following example shows the basic use of the `StringDecoder` class. + * + * ```js + * import { StringDecoder } from 'node:string_decoder'; + * const decoder = new StringDecoder('utf8'); + * + * const cent = Buffer.from([0xC2, 0xA2]); + * console.log(decoder.write(cent)); // Prints: ¢ + * + * const euro = Buffer.from([0xE2, 0x82, 0xAC]); + * console.log(decoder.write(euro)); // Prints: € + * ``` + * + * When a `Buffer` instance is written to the `StringDecoder` instance, an + * internal buffer is used to ensure that the decoded string does not contain + * any incomplete multibyte characters. These are held in the buffer until the + * next call to `stringDecoder.write()` or until `stringDecoder.end()` is called. + * + * In the following example, the three UTF-8 encoded bytes of the European Euro + * symbol (`€`) are written over three separate operations: + * + * ```js + * import { StringDecoder } from 'node:string_decoder'; + * const decoder = new StringDecoder('utf8'); + * + * decoder.write(Buffer.from([0xE2])); + * decoder.write(Buffer.from([0x82])); + * console.log(decoder.end(Buffer.from([0xAC]))); // Prints: € + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/string_decoder.js) + */ +declare module "string_decoder" { + class StringDecoder { + constructor(encoding?: BufferEncoding); + /** + * Returns a decoded string, ensuring that any incomplete multibyte characters at + * the end of the `Buffer`, or `TypedArray`, or `DataView` are omitted from the + * returned string and stored in an internal buffer for the next call to `stringDecoder.write()` or `stringDecoder.end()`. + * @since v0.1.99 + * @param buffer The bytes to decode. + */ + write(buffer: string | NodeJS.ArrayBufferView): string; + /** + * Returns any remaining input stored in the internal buffer as a string. Bytes + * representing incomplete UTF-8 and UTF-16 characters will be replaced with + * substitution characters appropriate for the character encoding. + * + * If the `buffer` argument is provided, one final call to `stringDecoder.write()` is performed before returning the remaining input. + * After `end()` is called, the `stringDecoder` object can be reused for new input. + * @since v0.9.3 + * @param buffer The bytes to decode. + */ + end(buffer?: string | NodeJS.ArrayBufferView): string; + } +} +declare module "node:string_decoder" { + export * from "string_decoder"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/test.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/test.d.ts new file mode 100644 index 00000000..1e9613a8 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/test.d.ts @@ -0,0 +1,2163 @@ +/** + * The `node:test` module facilitates the creation of JavaScript tests. + * To access it: + * + * ```js + * import test from 'node:test'; + * ``` + * + * This module is only available under the `node:` scheme. The following will not + * work: + * + * ```js + * import test from 'node:test'; + * ``` + * + * Tests created via the `test` module consist of a single function that is + * processed in one of three ways: + * + * 1. A synchronous function that is considered failing if it throws an exception, + * and is considered passing otherwise. + * 2. A function that returns a `Promise` that is considered failing if the `Promise` rejects, and is considered passing if the `Promise` fulfills. + * 3. A function that receives a callback function. If the callback receives any + * truthy value as its first argument, the test is considered failing. If a + * falsy value is passed as the first argument to the callback, the test is + * considered passing. If the test function receives a callback function and + * also returns a `Promise`, the test will fail. + * + * The following example illustrates how tests are written using the `test` module. + * + * ```js + * test('synchronous passing test', (t) => { + * // This test passes because it does not throw an exception. + * assert.strictEqual(1, 1); + * }); + * + * test('synchronous failing test', (t) => { + * // This test fails because it throws an exception. + * assert.strictEqual(1, 2); + * }); + * + * test('asynchronous passing test', async (t) => { + * // This test passes because the Promise returned by the async + * // function is settled and not rejected. + * assert.strictEqual(1, 1); + * }); + * + * test('asynchronous failing test', async (t) => { + * // This test fails because the Promise returned by the async + * // function is rejected. + * assert.strictEqual(1, 2); + * }); + * + * test('failing test using Promises', (t) => { + * // Promises can be used directly as well. + * return new Promise((resolve, reject) => { + * setImmediate(() => { + * reject(new Error('this will cause the test to fail')); + * }); + * }); + * }); + * + * test('callback passing test', (t, done) => { + * // done() is the callback function. When the setImmediate() runs, it invokes + * // done() with no arguments. + * setImmediate(done); + * }); + * + * test('callback failing test', (t, done) => { + * // When the setImmediate() runs, done() is invoked with an Error object and + * // the test fails. + * setImmediate(() => { + * done(new Error('callback failure')); + * }); + * }); + * ``` + * + * If any tests fail, the process exit code is set to `1`. + * @since v18.0.0, v16.17.0 + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/test.js) + */ +declare module "node:test" { + import { AssertMethodNames } from "node:assert"; + import { Readable } from "node:stream"; + import { URL } from "node:url"; + import TestFn = test.TestFn; + import TestOptions = test.TestOptions; + /** + * The `test()` function is the value imported from the `test` module. Each + * invocation of this function results in reporting the test to the `TestsStream`. + * + * The `TestContext` object passed to the `fn` argument can be used to perform + * actions related to the current test. Examples include skipping the test, adding + * additional diagnostic information, or creating subtests. + * + * `test()` returns a `Promise` that fulfills once the test completes. + * if `test()` is called within a suite, it fulfills immediately. + * The return value can usually be discarded for top level tests. + * However, the return value from subtests should be used to prevent the parent + * test from finishing first and cancelling the subtest + * as shown in the following example. + * + * ```js + * test('top level test', async (t) => { + * // The setTimeout() in the following subtest would cause it to outlive its + * // parent test if 'await' is removed on the next line. Once the parent test + * // completes, it will cancel any outstanding subtests. + * await t.test('longer running subtest', async (t) => { + * return new Promise((resolve, reject) => { + * setTimeout(resolve, 1000); + * }); + * }); + * }); + * ``` + * + * The `timeout` option can be used to fail the test if it takes longer than `timeout` milliseconds to complete. However, it is not a reliable mechanism for + * canceling tests because a running test might block the application thread and + * thus prevent the scheduled cancellation. + * @since v18.0.0, v16.17.0 + * @param name The name of the test, which is displayed when reporting test results. + * Defaults to the `name` property of `fn`, or `''` if `fn` does not have a name. + * @param options Configuration options for the test. + * @param fn The function under test. The first argument to this function is a {@link TestContext} object. + * If the test uses callbacks, the callback function is passed as the second argument. + * @return Fulfilled with `undefined` once the test completes, or immediately if the test runs within a suite. + */ + function test(name?: string, fn?: TestFn): Promise; + function test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function test(options?: TestOptions, fn?: TestFn): Promise; + function test(fn?: TestFn): Promise; + namespace test { + export { test }; + export { suite as describe, test as it }; + } + namespace test { + /** + * **Note:** `shard` is used to horizontally parallelize test running across + * machines or processes, ideal for large-scale executions across varied + * environments. It's incompatible with `watch` mode, tailored for rapid + * code iteration by automatically rerunning tests on file changes. + * + * ```js + * import { tap } from 'node:test/reporters'; + * import { run } from 'node:test'; + * import process from 'node:process'; + * import path from 'node:path'; + * + * run({ files: [path.resolve('./tests/test.js')] }) + * .compose(tap) + * .pipe(process.stdout); + * ``` + * @since v18.9.0, v16.19.0 + * @param options Configuration options for running tests. + */ + function run(options?: RunOptions): TestsStream; + /** + * The `suite()` function is imported from the `node:test` module. + * @param name The name of the suite, which is displayed when reporting test results. + * Defaults to the `name` property of `fn`, or `''` if `fn` does not have a name. + * @param options Configuration options for the suite. This supports the same options as {@link test}. + * @param fn The suite function declaring nested tests and suites. The first argument to this function is a {@link SuiteContext} object. + * @return Immediately fulfilled with `undefined`. + * @since v20.13.0 + */ + function suite(name?: string, options?: TestOptions, fn?: SuiteFn): Promise; + function suite(name?: string, fn?: SuiteFn): Promise; + function suite(options?: TestOptions, fn?: SuiteFn): Promise; + function suite(fn?: SuiteFn): Promise; + namespace suite { + /** + * Shorthand for skipping a suite. This is the same as calling {@link suite} with `options.skip` set to `true`. + * @since v20.13.0 + */ + function skip(name?: string, options?: TestOptions, fn?: SuiteFn): Promise; + function skip(name?: string, fn?: SuiteFn): Promise; + function skip(options?: TestOptions, fn?: SuiteFn): Promise; + function skip(fn?: SuiteFn): Promise; + /** + * Shorthand for marking a suite as `TODO`. This is the same as calling {@link suite} with `options.todo` set to `true`. + * @since v20.13.0 + */ + function todo(name?: string, options?: TestOptions, fn?: SuiteFn): Promise; + function todo(name?: string, fn?: SuiteFn): Promise; + function todo(options?: TestOptions, fn?: SuiteFn): Promise; + function todo(fn?: SuiteFn): Promise; + /** + * Shorthand for marking a suite as `only`. This is the same as calling {@link suite} with `options.only` set to `true`. + * @since v20.13.0 + */ + function only(name?: string, options?: TestOptions, fn?: SuiteFn): Promise; + function only(name?: string, fn?: SuiteFn): Promise; + function only(options?: TestOptions, fn?: SuiteFn): Promise; + function only(fn?: SuiteFn): Promise; + } + /** + * Shorthand for skipping a test. This is the same as calling {@link test} with `options.skip` set to `true`. + * @since v20.2.0 + */ + function skip(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function skip(name?: string, fn?: TestFn): Promise; + function skip(options?: TestOptions, fn?: TestFn): Promise; + function skip(fn?: TestFn): Promise; + /** + * Shorthand for marking a test as `TODO`. This is the same as calling {@link test} with `options.todo` set to `true`. + * @since v20.2.0 + */ + function todo(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function todo(name?: string, fn?: TestFn): Promise; + function todo(options?: TestOptions, fn?: TestFn): Promise; + function todo(fn?: TestFn): Promise; + /** + * Shorthand for marking a test as `only`. This is the same as calling {@link test} with `options.only` set to `true`. + * @since v20.2.0 + */ + function only(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function only(name?: string, fn?: TestFn): Promise; + function only(options?: TestOptions, fn?: TestFn): Promise; + function only(fn?: TestFn): Promise; + /** + * The type of a function passed to {@link test}. The first argument to this function is a {@link TestContext} object. + * If the test uses callbacks, the callback function is passed as the second argument. + */ + type TestFn = (t: TestContext, done: (result?: any) => void) => void | Promise; + /** + * The type of a suite test function. The argument to this function is a {@link SuiteContext} object. + */ + type SuiteFn = (s: SuiteContext) => void | Promise; + interface TestShard { + /** + * A positive integer between 1 and `total` that specifies the index of the shard to run. + */ + index: number; + /** + * A positive integer that specifies the total number of shards to split the test files to. + */ + total: number; + } + interface RunOptions { + /** + * If a number is provided, then that many test processes would run in parallel, where each process corresponds to one test file. + * If `true`, it would run `os.availableParallelism() - 1` test files in parallel. If `false`, it would only run one test file at a time. + * @default false + */ + concurrency?: number | boolean | undefined; + /** + * An array containing the list of files to run. If omitted, files are run according to the + * [test runner execution model](https://nodejs.org/docs/latest-v22.x/api/test.html#test-runner-execution-model). + */ + files?: readonly string[] | undefined; + /** + * Configures the test runner to exit the process once all known + * tests have finished executing even if the event loop would + * otherwise remain active. + * @default false + */ + forceExit?: boolean | undefined; + /** + * An array containing the list of glob patterns to match test files. + * This option cannot be used together with `files`. If omitted, files are run according to the + * [test runner execution model](https://nodejs.org/docs/latest-v22.x/api/test.html#test-runner-execution-model). + * @since v22.6.0 + */ + globPatterns?: readonly string[] | undefined; + /** + * Sets inspector port of test child process. + * This can be a number, or a function that takes no arguments and returns a + * number. If a nullish value is provided, each process gets its own port, + * incremented from the primary's `process.debugPort`. This option is ignored + * if the `isolation` option is set to `'none'` as no child processes are + * spawned. + * @default undefined + */ + inspectPort?: number | (() => number) | undefined; + /** + * Configures the type of test isolation. If set to + * `'process'`, each test file is run in a separate child process. If set to + * `'none'`, all test files run in the current process. + * @default 'process' + * @since v22.8.0 + */ + isolation?: "process" | "none" | undefined; + /** + * If truthy, the test context will only run tests that have the `only` option set + */ + only?: boolean | undefined; + /** + * A function that accepts the `TestsStream` instance and can be used to setup listeners before any tests are run. + * @default undefined + */ + setup?: ((reporter: TestsStream) => void | Promise) | undefined; + /** + * An array of CLI flags to pass to the `node` executable when + * spawning the subprocesses. This option has no effect when `isolation` is `'none`'. + * @since v22.10.0 + * @default [] + */ + execArgv?: readonly string[] | undefined; + /** + * An array of CLI flags to pass to each test file when spawning the + * subprocesses. This option has no effect when `isolation` is `'none'`. + * @since v22.10.0 + * @default [] + */ + argv?: readonly string[] | undefined; + /** + * Allows aborting an in-progress test execution. + */ + signal?: AbortSignal | undefined; + /** + * If provided, only run tests whose name matches the provided pattern. + * Strings are interpreted as JavaScript regular expressions. + * @default undefined + */ + testNamePatterns?: string | RegExp | ReadonlyArray | undefined; + /** + * A String, RegExp or a RegExp Array, that can be used to exclude running tests whose + * name matches the provided pattern. Test name patterns are interpreted as JavaScript + * regular expressions. For each test that is executed, any corresponding test hooks, + * such as `beforeEach()`, are also run. + * @default undefined + * @since v22.1.0 + */ + testSkipPatterns?: string | RegExp | ReadonlyArray | undefined; + /** + * The number of milliseconds after which the test execution will fail. + * If unspecified, subtests inherit this value from their parent. + * @default Infinity + */ + timeout?: number | undefined; + /** + * Whether to run in watch mode or not. + * @default false + */ + watch?: boolean | undefined; + /** + * Running tests in a specific shard. + * @default undefined + */ + shard?: TestShard | undefined; + /** + * enable [code coverage](https://nodejs.org/docs/latest-v22.x/api/test.html#collecting-code-coverage) collection. + * @since v22.10.0 + * @default false + */ + coverage?: boolean | undefined; + /** + * Excludes specific files from code coverage + * using a glob pattern, which can match both absolute and relative file paths. + * This property is only applicable when `coverage` was set to `true`. + * If both `coverageExcludeGlobs` and `coverageIncludeGlobs` are provided, + * files must meet **both** criteria to be included in the coverage report. + * @since v22.10.0 + * @default undefined + */ + coverageExcludeGlobs?: string | readonly string[] | undefined; + /** + * Includes specific files in code coverage + * using a glob pattern, which can match both absolute and relative file paths. + * This property is only applicable when `coverage` was set to `true`. + * If both `coverageExcludeGlobs` and `coverageIncludeGlobs` are provided, + * files must meet **both** criteria to be included in the coverage report. + * @since v22.10.0 + * @default undefined + */ + coverageIncludeGlobs?: string | readonly string[] | undefined; + /** + * Require a minimum percent of covered lines. If code + * coverage does not reach the threshold specified, the process will exit with code `1`. + * @since v22.10.0 + * @default 0 + */ + lineCoverage?: number | undefined; + /** + * Require a minimum percent of covered branches. If code + * coverage does not reach the threshold specified, the process will exit with code `1`. + * @since v22.10.0 + * @default 0 + */ + branchCoverage?: number | undefined; + /** + * Require a minimum percent of covered functions. If code + * coverage does not reach the threshold specified, the process will exit with code `1`. + * @since v22.10.0 + * @default 0 + */ + functionCoverage?: number | undefined; + } + /** + * A successful call to `run()` will return a new `TestsStream` object, streaming a series of events representing the execution of the tests. + * + * Some of the events are guaranteed to be emitted in the same order as the tests are defined, while others are emitted in the order that the tests execute. + * @since v18.9.0, v16.19.0 + */ + interface TestsStream extends Readable { + addListener(event: "test:coverage", listener: (data: EventData.TestCoverage) => void): this; + addListener(event: "test:complete", listener: (data: EventData.TestComplete) => void): this; + addListener(event: "test:dequeue", listener: (data: EventData.TestDequeue) => void): this; + addListener(event: "test:diagnostic", listener: (data: EventData.TestDiagnostic) => void): this; + addListener(event: "test:enqueue", listener: (data: EventData.TestEnqueue) => void): this; + addListener(event: "test:fail", listener: (data: EventData.TestFail) => void): this; + addListener(event: "test:pass", listener: (data: EventData.TestPass) => void): this; + addListener(event: "test:plan", listener: (data: EventData.TestPlan) => void): this; + addListener(event: "test:start", listener: (data: EventData.TestStart) => void): this; + addListener(event: "test:stderr", listener: (data: EventData.TestStderr) => void): this; + addListener(event: "test:stdout", listener: (data: EventData.TestStdout) => void): this; + addListener(event: "test:summary", listener: (data: EventData.TestSummary) => void): this; + addListener(event: "test:watch:drained", listener: () => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + emit(event: "test:coverage", data: EventData.TestCoverage): boolean; + emit(event: "test:complete", data: EventData.TestComplete): boolean; + emit(event: "test:dequeue", data: EventData.TestDequeue): boolean; + emit(event: "test:diagnostic", data: EventData.TestDiagnostic): boolean; + emit(event: "test:enqueue", data: EventData.TestEnqueue): boolean; + emit(event: "test:fail", data: EventData.TestFail): boolean; + emit(event: "test:pass", data: EventData.TestPass): boolean; + emit(event: "test:plan", data: EventData.TestPlan): boolean; + emit(event: "test:start", data: EventData.TestStart): boolean; + emit(event: "test:stderr", data: EventData.TestStderr): boolean; + emit(event: "test:stdout", data: EventData.TestStdout): boolean; + emit(event: "test:summary", data: EventData.TestSummary): boolean; + emit(event: "test:watch:drained"): boolean; + emit(event: string | symbol, ...args: any[]): boolean; + on(event: "test:coverage", listener: (data: EventData.TestCoverage) => void): this; + on(event: "test:complete", listener: (data: EventData.TestComplete) => void): this; + on(event: "test:dequeue", listener: (data: EventData.TestDequeue) => void): this; + on(event: "test:diagnostic", listener: (data: EventData.TestDiagnostic) => void): this; + on(event: "test:enqueue", listener: (data: EventData.TestEnqueue) => void): this; + on(event: "test:fail", listener: (data: EventData.TestFail) => void): this; + on(event: "test:pass", listener: (data: EventData.TestPass) => void): this; + on(event: "test:plan", listener: (data: EventData.TestPlan) => void): this; + on(event: "test:start", listener: (data: EventData.TestStart) => void): this; + on(event: "test:stderr", listener: (data: EventData.TestStderr) => void): this; + on(event: "test:stdout", listener: (data: EventData.TestStdout) => void): this; + on(event: "test:summary", listener: (data: EventData.TestSummary) => void): this; + on(event: "test:watch:drained", listener: () => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "test:coverage", listener: (data: EventData.TestCoverage) => void): this; + once(event: "test:complete", listener: (data: EventData.TestComplete) => void): this; + once(event: "test:dequeue", listener: (data: EventData.TestDequeue) => void): this; + once(event: "test:diagnostic", listener: (data: EventData.TestDiagnostic) => void): this; + once(event: "test:enqueue", listener: (data: EventData.TestEnqueue) => void): this; + once(event: "test:fail", listener: (data: EventData.TestFail) => void): this; + once(event: "test:pass", listener: (data: EventData.TestPass) => void): this; + once(event: "test:plan", listener: (data: EventData.TestPlan) => void): this; + once(event: "test:start", listener: (data: EventData.TestStart) => void): this; + once(event: "test:stderr", listener: (data: EventData.TestStderr) => void): this; + once(event: "test:stdout", listener: (data: EventData.TestStdout) => void): this; + once(event: "test:summary", listener: (data: EventData.TestSummary) => void): this; + once(event: "test:watch:drained", listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "test:coverage", listener: (data: EventData.TestCoverage) => void): this; + prependListener(event: "test:complete", listener: (data: EventData.TestComplete) => void): this; + prependListener(event: "test:dequeue", listener: (data: EventData.TestDequeue) => void): this; + prependListener(event: "test:diagnostic", listener: (data: EventData.TestDiagnostic) => void): this; + prependListener(event: "test:enqueue", listener: (data: EventData.TestEnqueue) => void): this; + prependListener(event: "test:fail", listener: (data: EventData.TestFail) => void): this; + prependListener(event: "test:pass", listener: (data: EventData.TestPass) => void): this; + prependListener(event: "test:plan", listener: (data: EventData.TestPlan) => void): this; + prependListener(event: "test:start", listener: (data: EventData.TestStart) => void): this; + prependListener(event: "test:stderr", listener: (data: EventData.TestStderr) => void): this; + prependListener(event: "test:stdout", listener: (data: EventData.TestStdout) => void): this; + prependListener(event: "test:summary", listener: (data: EventData.TestSummary) => void): this; + prependListener(event: "test:watch:drained", listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "test:coverage", listener: (data: EventData.TestCoverage) => void): this; + prependOnceListener(event: "test:complete", listener: (data: EventData.TestComplete) => void): this; + prependOnceListener(event: "test:dequeue", listener: (data: EventData.TestDequeue) => void): this; + prependOnceListener(event: "test:diagnostic", listener: (data: EventData.TestDiagnostic) => void): this; + prependOnceListener(event: "test:enqueue", listener: (data: EventData.TestEnqueue) => void): this; + prependOnceListener(event: "test:fail", listener: (data: EventData.TestFail) => void): this; + prependOnceListener(event: "test:pass", listener: (data: EventData.TestPass) => void): this; + prependOnceListener(event: "test:plan", listener: (data: EventData.TestPlan) => void): this; + prependOnceListener(event: "test:start", listener: (data: EventData.TestStart) => void): this; + prependOnceListener(event: "test:stderr", listener: (data: EventData.TestStderr) => void): this; + prependOnceListener(event: "test:stdout", listener: (data: EventData.TestStdout) => void): this; + prependOnceListener(event: "test:summary", listener: (data: EventData.TestSummary) => void): this; + prependOnceListener(event: "test:watch:drained", listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + } + namespace EventData { + interface Error extends globalThis.Error { + cause: globalThis.Error; + } + interface LocationInfo { + /** + * The column number where the test is defined, or + * `undefined` if the test was run through the REPL. + */ + column?: number; + /** + * The path of the test file, `undefined` if test was run through the REPL. + */ + file?: string; + /** + * The line number where the test is defined, or `undefined` if the test was run through the REPL. + */ + line?: number; + } + interface TestDiagnostic extends LocationInfo { + /** + * The diagnostic message. + */ + message: string; + /** + * The nesting level of the test. + */ + nesting: number; + /** + * The severity level of the diagnostic message. + * Possible values are: + * * `'info'`: Informational messages. + * * `'warn'`: Warnings. + * * `'error'`: Errors. + */ + level: "info" | "warn" | "error"; + } + interface TestCoverage { + /** + * An object containing the coverage report. + */ + summary: { + /** + * An array of coverage reports for individual files. + */ + files: Array<{ + /** + * The absolute path of the file. + */ + path: string; + /** + * The total number of lines. + */ + totalLineCount: number; + /** + * The total number of branches. + */ + totalBranchCount: number; + /** + * The total number of functions. + */ + totalFunctionCount: number; + /** + * The number of covered lines. + */ + coveredLineCount: number; + /** + * The number of covered branches. + */ + coveredBranchCount: number; + /** + * The number of covered functions. + */ + coveredFunctionCount: number; + /** + * The percentage of lines covered. + */ + coveredLinePercent: number; + /** + * The percentage of branches covered. + */ + coveredBranchPercent: number; + /** + * The percentage of functions covered. + */ + coveredFunctionPercent: number; + /** + * An array of functions representing function coverage. + */ + functions: Array<{ + /** + * The name of the function. + */ + name: string; + /** + * The line number where the function is defined. + */ + line: number; + /** + * The number of times the function was called. + */ + count: number; + }>; + /** + * An array of branches representing branch coverage. + */ + branches: Array<{ + /** + * The line number where the branch is defined. + */ + line: number; + /** + * The number of times the branch was taken. + */ + count: number; + }>; + /** + * An array of lines representing line numbers and the number of times they were covered. + */ + lines: Array<{ + /** + * The line number. + */ + line: number; + /** + * The number of times the line was covered. + */ + count: number; + }>; + }>; + /** + * An object containing whether or not the coverage for + * each coverage type. + * @since v22.9.0 + */ + thresholds: { + /** + * The function coverage threshold. + */ + function: number; + /** + * The branch coverage threshold. + */ + branch: number; + /** + * The line coverage threshold. + */ + line: number; + }; + /** + * An object containing a summary of coverage for all files. + */ + totals: { + /** + * The total number of lines. + */ + totalLineCount: number; + /** + * The total number of branches. + */ + totalBranchCount: number; + /** + * The total number of functions. + */ + totalFunctionCount: number; + /** + * The number of covered lines. + */ + coveredLineCount: number; + /** + * The number of covered branches. + */ + coveredBranchCount: number; + /** + * The number of covered functions. + */ + coveredFunctionCount: number; + /** + * The percentage of lines covered. + */ + coveredLinePercent: number; + /** + * The percentage of branches covered. + */ + coveredBranchPercent: number; + /** + * The percentage of functions covered. + */ + coveredFunctionPercent: number; + }; + /** + * The working directory when code coverage began. This + * is useful for displaying relative path names in case + * the tests changed the working directory of the Node.js process. + */ + workingDirectory: string; + }; + /** + * The nesting level of the test. + */ + nesting: number; + } + interface TestComplete extends LocationInfo { + /** + * Additional execution metadata. + */ + details: { + /** + * Whether the test passed or not. + */ + passed: boolean; + /** + * The duration of the test in milliseconds. + */ + duration_ms: number; + /** + * An error wrapping the error thrown by the test if it did not pass. + */ + error?: Error; + /** + * The type of the test, used to denote whether this is a suite. + */ + type?: "suite"; + }; + /** + * The test name. + */ + name: string; + /** + * The nesting level of the test. + */ + nesting: number; + /** + * The ordinal number of the test. + */ + testNumber: number; + /** + * Present if `context.todo` is called. + */ + todo?: string | boolean; + /** + * Present if `context.skip` is called. + */ + skip?: string | boolean; + } + interface TestDequeue extends LocationInfo { + /** + * The test name. + */ + name: string; + /** + * The nesting level of the test. + */ + nesting: number; + /** + * The test type. Either `'suite'` or `'test'`. + * @since v22.15.0 + */ + type: "suite" | "test"; + } + interface TestEnqueue extends LocationInfo { + /** + * The test name. + */ + name: string; + /** + * The nesting level of the test. + */ + nesting: number; + /** + * The test type. Either `'suite'` or `'test'`. + * @since v22.15.0 + */ + type: "suite" | "test"; + } + interface TestFail extends LocationInfo { + /** + * Additional execution metadata. + */ + details: { + /** + * The duration of the test in milliseconds. + */ + duration_ms: number; + /** + * An error wrapping the error thrown by the test. + */ + error: Error; + /** + * The type of the test, used to denote whether this is a suite. + * @since v20.0.0, v19.9.0, v18.17.0 + */ + type?: "suite"; + }; + /** + * The test name. + */ + name: string; + /** + * The nesting level of the test. + */ + nesting: number; + /** + * The ordinal number of the test. + */ + testNumber: number; + /** + * Present if `context.todo` is called. + */ + todo?: string | boolean; + /** + * Present if `context.skip` is called. + */ + skip?: string | boolean; + } + interface TestPass extends LocationInfo { + /** + * Additional execution metadata. + */ + details: { + /** + * The duration of the test in milliseconds. + */ + duration_ms: number; + /** + * The type of the test, used to denote whether this is a suite. + * @since 20.0.0, 19.9.0, 18.17.0 + */ + type?: "suite"; + }; + /** + * The test name. + */ + name: string; + /** + * The nesting level of the test. + */ + nesting: number; + /** + * The ordinal number of the test. + */ + testNumber: number; + /** + * Present if `context.todo` is called. + */ + todo?: string | boolean; + /** + * Present if `context.skip` is called. + */ + skip?: string | boolean; + } + interface TestPlan extends LocationInfo { + /** + * The nesting level of the test. + */ + nesting: number; + /** + * The number of subtests that have ran. + */ + count: number; + } + interface TestStart extends LocationInfo { + /** + * The test name. + */ + name: string; + /** + * The nesting level of the test. + */ + nesting: number; + } + interface TestStderr { + /** + * The path of the test file. + */ + file: string; + /** + * The message written to `stderr`. + */ + message: string; + } + interface TestStdout { + /** + * The path of the test file. + */ + file: string; + /** + * The message written to `stdout`. + */ + message: string; + } + interface TestSummary { + /** + * An object containing the counts of various test results. + */ + counts: { + /** + * The total number of cancelled tests. + */ + cancelled: number; + /** + * The total number of passed tests. + */ + passed: number; + /** + * The total number of skipped tests. + */ + skipped: number; + /** + * The total number of suites run. + */ + suites: number; + /** + * The total number of tests run, excluding suites. + */ + tests: number; + /** + * The total number of TODO tests. + */ + todo: number; + /** + * The total number of top level tests and suites. + */ + topLevel: number; + }; + /** + * The duration of the test run in milliseconds. + */ + duration_ms: number; + /** + * The path of the test file that generated the + * summary. If the summary corresponds to multiple files, this value is + * `undefined`. + */ + file: string | undefined; + /** + * Indicates whether or not the test run is considered + * successful or not. If any error condition occurs, such as a failing test or + * unmet coverage threshold, this value will be set to `false`. + */ + success: boolean; + } + } + /** + * An instance of `TestContext` is passed to each test function in order to + * interact with the test runner. However, the `TestContext` constructor is not + * exposed as part of the API. + * @since v18.0.0, v16.17.0 + */ + interface TestContext { + /** + * An object containing assertion methods bound to the test context. + * The top-level functions from the `node:assert` module are exposed here for the purpose of creating test plans. + * + * **Note:** Some of the functions from `node:assert` contain type assertions. If these are called via the + * TestContext `assert` object, then the context parameter in the test's function signature **must be explicitly typed** + * (ie. the parameter must have a type annotation), otherwise an error will be raised by the TypeScript compiler: + * ```ts + * import { test, type TestContext } from 'node:test'; + * + * // The test function's context parameter must have a type annotation. + * test('example', (t: TestContext) => { + * t.assert.deepStrictEqual(actual, expected); + * }); + * + * // Omitting the type annotation will result in a compilation error. + * test('example', t => { + * t.assert.deepStrictEqual(actual, expected); // Error: 't' needs an explicit type annotation. + * }); + * ``` + * @since v22.2.0, v20.15.0 + */ + readonly assert: TestContextAssert; + /** + * This function is used to create a hook running before subtest of the current test. + * @param fn The hook function. The first argument to this function is a `TestContext` object. + * If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + * @since v20.1.0, v18.17.0 + */ + before(fn?: TestContextHookFn, options?: HookOptions): void; + /** + * This function is used to create a hook running before each subtest of the current test. + * @param fn The hook function. The first argument to this function is a `TestContext` object. + * If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + * @since v18.8.0 + */ + beforeEach(fn?: TestContextHookFn, options?: HookOptions): void; + /** + * This function is used to create a hook that runs after the current test finishes. + * @param fn The hook function. The first argument to this function is a `TestContext` object. + * If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + * @since v18.13.0 + */ + after(fn?: TestContextHookFn, options?: HookOptions): void; + /** + * This function is used to create a hook running after each subtest of the current test. + * @param fn The hook function. The first argument to this function is a `TestContext` object. + * If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + * @since v18.8.0 + */ + afterEach(fn?: TestContextHookFn, options?: HookOptions): void; + /** + * This function is used to write diagnostics to the output. Any diagnostic + * information is included at the end of the test's results. This function does + * not return a value. + * + * ```js + * test('top level test', (t) => { + * t.diagnostic('A diagnostic message'); + * }); + * ``` + * @since v18.0.0, v16.17.0 + * @param message Message to be reported. + */ + diagnostic(message: string): void; + /** + * The absolute path of the test file that created the current test. If a test file imports + * additional modules that generate tests, the imported tests will return the path of the root test file. + * @since v22.6.0 + */ + readonly filePath: string | undefined; + /** + * The name of the test and each of its ancestors, separated by `>`. + * @since v22.3.0 + */ + readonly fullName: string; + /** + * The name of the test. + * @since v18.8.0, v16.18.0 + */ + readonly name: string; + /** + * This function is used to set the number of assertions and subtests that are expected to run + * within the test. If the number of assertions and subtests that run does not match the + * expected count, the test will fail. + * + * > Note: To make sure assertions are tracked, `t.assert` must be used instead of `assert` directly. + * + * ```js + * test('top level test', (t) => { + * t.plan(2); + * t.assert.ok('some relevant assertion here'); + * t.test('subtest', () => {}); + * }); + * ``` + * + * When working with asynchronous code, the `plan` function can be used to ensure that the + * correct number of assertions are run: + * + * ```js + * test('planning with streams', (t, done) => { + * function* generate() { + * yield 'a'; + * yield 'b'; + * yield 'c'; + * } + * const expected = ['a', 'b', 'c']; + * t.plan(expected.length); + * const stream = Readable.from(generate()); + * stream.on('data', (chunk) => { + * t.assert.strictEqual(chunk, expected.shift()); + * }); + * + * stream.on('end', () => { + * done(); + * }); + * }); + * ``` + * + * When using the `wait` option, you can control how long the test will wait for the expected assertions. + * For example, setting a maximum wait time ensures that the test will wait for asynchronous assertions + * to complete within the specified timeframe: + * + * ```js + * test('plan with wait: 2000 waits for async assertions', (t) => { + * t.plan(1, { wait: 2000 }); // Waits for up to 2 seconds for the assertion to complete. + * + * const asyncActivity = () => { + * setTimeout(() => { + * * t.assert.ok(true, 'Async assertion completed within the wait time'); + * }, 1000); // Completes after 1 second, within the 2-second wait time. + * }; + * + * asyncActivity(); // The test will pass because the assertion is completed in time. + * }); + * ``` + * + * Note: If a `wait` timeout is specified, it begins counting down only after the test function finishes executing. + * @since v22.2.0 + */ + plan(count: number, options?: TestContextPlanOptions): void; + /** + * If `shouldRunOnlyTests` is truthy, the test context will only run tests that + * have the `only` option set. Otherwise, all tests are run. If Node.js was not + * started with the `--test-only` command-line option, this function is a + * no-op. + * + * ```js + * test('top level test', (t) => { + * // The test context can be set to run subtests with the 'only' option. + * t.runOnly(true); + * return Promise.all([ + * t.test('this subtest is now skipped'), + * t.test('this subtest is run', { only: true }), + * ]); + * }); + * ``` + * @since v18.0.0, v16.17.0 + * @param shouldRunOnlyTests Whether or not to run `only` tests. + */ + runOnly(shouldRunOnlyTests: boolean): void; + /** + * ```js + * test('top level test', async (t) => { + * await fetch('some/uri', { signal: t.signal }); + * }); + * ``` + * @since v18.7.0, v16.17.0 + */ + readonly signal: AbortSignal; + /** + * This function causes the test's output to indicate the test as skipped. If `message` is provided, it is included in the output. Calling `skip()` does + * not terminate execution of the test function. This function does not return a + * value. + * + * ```js + * test('top level test', (t) => { + * // Make sure to return here as well if the test contains additional logic. + * t.skip('this is skipped'); + * }); + * ``` + * @since v18.0.0, v16.17.0 + * @param message Optional skip message. + */ + skip(message?: string): void; + /** + * This function adds a `TODO` directive to the test's output. If `message` is + * provided, it is included in the output. Calling `todo()` does not terminate + * execution of the test function. This function does not return a value. + * + * ```js + * test('top level test', (t) => { + * // This test is marked as `TODO` + * t.todo('this is a todo'); + * }); + * ``` + * @since v18.0.0, v16.17.0 + * @param message Optional `TODO` message. + */ + todo(message?: string): void; + /** + * This function is used to create subtests under the current test. This function behaves in + * the same fashion as the top level {@link test} function. + * @since v18.0.0 + * @param name The name of the test, which is displayed when reporting test results. + * Defaults to the `name` property of `fn`, or `''` if `fn` does not have a name. + * @param options Configuration options for the test. + * @param fn The function under test. This first argument to this function is a {@link TestContext} object. + * If the test uses callbacks, the callback function is passed as the second argument. + * @returns A {@link Promise} resolved with `undefined` once the test completes. + */ + test: typeof test; + /** + * This method polls a `condition` function until that function either returns + * successfully or the operation times out. + * @since v22.14.0 + * @param condition An assertion function that is invoked + * periodically until it completes successfully or the defined polling timeout + * elapses. Successful completion is defined as not throwing or rejecting. This + * function does not accept any arguments, and is allowed to return any value. + * @param options An optional configuration object for the polling operation. + * @returns Fulfilled with the value returned by `condition`. + */ + waitFor(condition: () => T, options?: TestContextWaitForOptions): Promise>; + /** + * Each test provides its own MockTracker instance. + */ + readonly mock: MockTracker; + } + interface TestContextAssert extends Pick { + /** + * This function serializes `value` and writes it to the file specified by `path`. + * + * ```js + * test('snapshot test with default serialization', (t) => { + * t.assert.fileSnapshot({ value1: 1, value2: 2 }, './snapshots/snapshot.json'); + * }); + * ``` + * + * This function differs from `context.assert.snapshot()` in the following ways: + * + * * The snapshot file path is explicitly provided by the user. + * * Each snapshot file is limited to a single snapshot value. + * * No additional escaping is performed by the test runner. + * + * These differences allow snapshot files to better support features such as syntax + * highlighting. + * @since v22.14.0 + * @param value A value to serialize to a string. If Node.js was started with + * the [`--test-update-snapshots`](https://nodejs.org/docs/latest-v22.x/api/cli.html#--test-update-snapshots) + * flag, the serialized value is written to + * `path`. Otherwise, the serialized value is compared to the contents of the + * existing snapshot file. + * @param path The file where the serialized `value` is written. + * @param options Optional configuration options. + */ + fileSnapshot(value: any, path: string, options?: AssertSnapshotOptions): void; + /** + * This function implements assertions for snapshot testing. + * ```js + * test('snapshot test with default serialization', (t) => { + * t.assert.snapshot({ value1: 1, value2: 2 }); + * }); + * + * test('snapshot test with custom serialization', (t) => { + * t.assert.snapshot({ value3: 3, value4: 4 }, { + * serializers: [(value) => JSON.stringify(value)] + * }); + * }); + * ``` + * @since v22.3.0 + * @param value A value to serialize to a string. If Node.js was started with + * the [`--test-update-snapshots`](https://nodejs.org/docs/latest-v22.x/api/cli.html#--test-update-snapshots) + * flag, the serialized value is written to + * the snapshot file. Otherwise, the serialized value is compared to the + * corresponding value in the existing snapshot file. + */ + snapshot(value: any, options?: AssertSnapshotOptions): void; + /** + * A custom assertion function registered with `assert.register()`. + */ + [name: string]: (...args: any[]) => void; + } + interface AssertSnapshotOptions { + /** + * An array of synchronous functions used to serialize `value` into a string. + * `value` is passed as the only argument to the first serializer function. + * The return value of each serializer is passed as input to the next serializer. + * Once all serializers have run, the resulting value is coerced to a string. + * + * If no serializers are provided, the test runner's default serializers are used. + */ + serializers?: ReadonlyArray<(value: any) => any> | undefined; + } + interface TestContextPlanOptions { + /** + * The wait time for the plan: + * * If `true`, the plan waits indefinitely for all assertions and subtests to run. + * * If `false`, the plan performs an immediate check after the test function completes, + * without waiting for any pending assertions or subtests. + * Any assertions or subtests that complete after this check will not be counted towards the plan. + * * If a number, it specifies the maximum wait time in milliseconds + * before timing out while waiting for expected assertions and subtests to be matched. + * If the timeout is reached, the test will fail. + * @default false + */ + wait?: boolean | number | undefined; + } + interface TestContextWaitForOptions { + /** + * The number of milliseconds to wait after an unsuccessful + * invocation of `condition` before trying again. + * @default 50 + */ + interval?: number | undefined; + /** + * The poll timeout in milliseconds. If `condition` has not + * succeeded by the time this elapses, an error occurs. + * @default 1000 + */ + timeout?: number | undefined; + } + /** + * An instance of `SuiteContext` is passed to each suite function in order to + * interact with the test runner. However, the `SuiteContext` constructor is not + * exposed as part of the API. + * @since v18.7.0, v16.17.0 + */ + interface SuiteContext { + /** + * The absolute path of the test file that created the current suite. If a test file imports + * additional modules that generate suites, the imported suites will return the path of the root test file. + * @since v22.6.0 + */ + readonly filePath: string | undefined; + /** + * The name of the suite. + * @since v18.8.0, v16.18.0 + */ + readonly name: string; + /** + * Can be used to abort test subtasks when the test has been aborted. + * @since v18.7.0, v16.17.0 + */ + readonly signal: AbortSignal; + } + interface TestOptions { + /** + * If a number is provided, then that many tests would run in parallel. + * If truthy, it would run (number of cpu cores - 1) tests in parallel. + * For subtests, it will be `Infinity` tests in parallel. + * If falsy, it would only run one test at a time. + * If unspecified, subtests inherit this value from their parent. + * @default false + */ + concurrency?: number | boolean | undefined; + /** + * If truthy, and the test context is configured to run `only` tests, then this test will be + * run. Otherwise, the test is skipped. + * @default false + */ + only?: boolean | undefined; + /** + * Allows aborting an in-progress test. + * @since v18.8.0 + */ + signal?: AbortSignal | undefined; + /** + * If truthy, the test is skipped. If a string is provided, that string is displayed in the + * test results as the reason for skipping the test. + * @default false + */ + skip?: boolean | string | undefined; + /** + * A number of milliseconds the test will fail after. If unspecified, subtests inherit this + * value from their parent. + * @default Infinity + * @since v18.7.0 + */ + timeout?: number | undefined; + /** + * If truthy, the test marked as `TODO`. If a string is provided, that string is displayed in + * the test results as the reason why the test is `TODO`. + * @default false + */ + todo?: boolean | string | undefined; + /** + * The number of assertions and subtests expected to be run in the test. + * If the number of assertions run in the test does not match the number + * specified in the plan, the test will fail. + * @default undefined + * @since v22.2.0 + */ + plan?: number | undefined; + } + /** + * This function creates a hook that runs before executing a suite. + * + * ```js + * describe('tests', async () => { + * before(() => console.log('about to run some test')); + * it('is a subtest', () => { + * assert.ok('some relevant assertion here'); + * }); + * }); + * ``` + * @since v18.8.0, v16.18.0 + * @param fn The hook function. If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + */ + function before(fn?: HookFn, options?: HookOptions): void; + /** + * This function creates a hook that runs after executing a suite. + * + * ```js + * describe('tests', async () => { + * after(() => console.log('finished running tests')); + * it('is a subtest', () => { + * assert.ok('some relevant assertion here'); + * }); + * }); + * ``` + * @since v18.8.0, v16.18.0 + * @param fn The hook function. If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + */ + function after(fn?: HookFn, options?: HookOptions): void; + /** + * This function creates a hook that runs before each test in the current suite. + * + * ```js + * describe('tests', async () => { + * beforeEach(() => console.log('about to run a test')); + * it('is a subtest', () => { + * assert.ok('some relevant assertion here'); + * }); + * }); + * ``` + * @since v18.8.0, v16.18.0 + * @param fn The hook function. If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + */ + function beforeEach(fn?: HookFn, options?: HookOptions): void; + /** + * This function creates a hook that runs after each test in the current suite. + * The `afterEach()` hook is run even if the test fails. + * + * ```js + * describe('tests', async () => { + * afterEach(() => console.log('finished running a test')); + * it('is a subtest', () => { + * assert.ok('some relevant assertion here'); + * }); + * }); + * ``` + * @since v18.8.0, v16.18.0 + * @param fn The hook function. If the hook uses callbacks, the callback function is passed as the second argument. + * @param options Configuration options for the hook. + */ + function afterEach(fn?: HookFn, options?: HookOptions): void; + /** + * The hook function. The first argument is the context in which the hook is called. + * If the hook uses callbacks, the callback function is passed as the second argument. + */ + type HookFn = (c: TestContext | SuiteContext, done: (result?: any) => void) => any; + /** + * The hook function. The first argument is a `TestContext` object. + * If the hook uses callbacks, the callback function is passed as the second argument. + */ + type TestContextHookFn = (t: TestContext, done: (result?: any) => void) => any; + /** + * Configuration options for hooks. + * @since v18.8.0 + */ + interface HookOptions { + /** + * Allows aborting an in-progress hook. + */ + signal?: AbortSignal | undefined; + /** + * A number of milliseconds the hook will fail after. If unspecified, subtests inherit this + * value from their parent. + * @default Infinity + */ + timeout?: number | undefined; + } + interface MockFunctionOptions { + /** + * The number of times that the mock will use the behavior of `implementation`. + * Once the mock function has been called `times` times, + * it will automatically restore the behavior of `original`. + * This value must be an integer greater than zero. + * @default Infinity + */ + times?: number | undefined; + } + interface MockMethodOptions extends MockFunctionOptions { + /** + * If `true`, `object[methodName]` is treated as a getter. + * This option cannot be used with the `setter` option. + */ + getter?: boolean | undefined; + /** + * If `true`, `object[methodName]` is treated as a setter. + * This option cannot be used with the `getter` option. + */ + setter?: boolean | undefined; + } + type Mock = F & { + mock: MockFunctionContext; + }; + interface MockModuleOptions { + /** + * If false, each call to `require()` or `import()` generates a new mock module. + * If true, subsequent calls will return the same module mock, and the mock module is inserted into the CommonJS cache. + * @default false + */ + cache?: boolean | undefined; + /** + * The value to use as the mocked module's default export. + * + * If this value is not provided, ESM mocks do not include a default export. + * If the mock is a CommonJS or builtin module, this setting is used as the value of `module.exports`. + * If this value is not provided, CJS and builtin mocks use an empty object as the value of `module.exports`. + */ + defaultExport?: any; + /** + * An object whose keys and values are used to create the named exports of the mock module. + * + * If the mock is a CommonJS or builtin module, these values are copied onto `module.exports`. + * Therefore, if a mock is created with both named exports and a non-object default export, + * the mock will throw an exception when used as a CJS or builtin module. + */ + namedExports?: object | undefined; + } + /** + * The `MockTracker` class is used to manage mocking functionality. The test runner + * module provides a top level `mock` export which is a `MockTracker` instance. + * Each test also provides its own `MockTracker` instance via the test context's `mock` property. + * @since v19.1.0, v18.13.0 + */ + interface MockTracker { + /** + * This function is used to create a mock function. + * + * The following example creates a mock function that increments a counter by one + * on each invocation. The `times` option is used to modify the mock behavior such + * that the first two invocations add two to the counter instead of one. + * + * ```js + * test('mocks a counting function', (t) => { + * let cnt = 0; + * + * function addOne() { + * cnt++; + * return cnt; + * } + * + * function addTwo() { + * cnt += 2; + * return cnt; + * } + * + * const fn = t.mock.fn(addOne, addTwo, { times: 2 }); + * + * assert.strictEqual(fn(), 2); + * assert.strictEqual(fn(), 4); + * assert.strictEqual(fn(), 5); + * assert.strictEqual(fn(), 6); + * }); + * ``` + * @since v19.1.0, v18.13.0 + * @param original An optional function to create a mock on. + * @param implementation An optional function used as the mock implementation for `original`. This is useful for creating mocks that exhibit one behavior for a specified number of calls and + * then restore the behavior of `original`. + * @param options Optional configuration options for the mock function. + * @return The mocked function. The mocked function contains a special `mock` property, which is an instance of {@link MockFunctionContext}, and can be used for inspecting and changing the + * behavior of the mocked function. + */ + fn undefined>( + original?: F, + options?: MockFunctionOptions, + ): Mock; + fn undefined, Implementation extends Function = F>( + original?: F, + implementation?: Implementation, + options?: MockFunctionOptions, + ): Mock; + /** + * This function is used to create a mock on an existing object method. The + * following example demonstrates how a mock is created on an existing object + * method. + * + * ```js + * test('spies on an object method', (t) => { + * const number = { + * value: 5, + * subtract(a) { + * return this.value - a; + * }, + * }; + * + * t.mock.method(number, 'subtract'); + * assert.strictEqual(number.subtract.mock.calls.length, 0); + * assert.strictEqual(number.subtract(3), 2); + * assert.strictEqual(number.subtract.mock.calls.length, 1); + * + * const call = number.subtract.mock.calls[0]; + * + * assert.deepStrictEqual(call.arguments, [3]); + * assert.strictEqual(call.result, 2); + * assert.strictEqual(call.error, undefined); + * assert.strictEqual(call.target, undefined); + * assert.strictEqual(call.this, number); + * }); + * ``` + * @since v19.1.0, v18.13.0 + * @param object The object whose method is being mocked. + * @param methodName The identifier of the method on `object` to mock. If `object[methodName]` is not a function, an error is thrown. + * @param implementation An optional function used as the mock implementation for `object[methodName]`. + * @param options Optional configuration options for the mock method. + * @return The mocked method. The mocked method contains a special `mock` property, which is an instance of {@link MockFunctionContext}, and can be used for inspecting and changing the + * behavior of the mocked method. + */ + method< + MockedObject extends object, + MethodName extends FunctionPropertyNames, + >( + object: MockedObject, + methodName: MethodName, + options?: MockFunctionOptions, + ): MockedObject[MethodName] extends Function ? Mock + : never; + method< + MockedObject extends object, + MethodName extends FunctionPropertyNames, + Implementation extends Function, + >( + object: MockedObject, + methodName: MethodName, + implementation: Implementation, + options?: MockFunctionOptions, + ): MockedObject[MethodName] extends Function ? Mock + : never; + method( + object: MockedObject, + methodName: keyof MockedObject, + options: MockMethodOptions, + ): Mock; + method( + object: MockedObject, + methodName: keyof MockedObject, + implementation: Function, + options: MockMethodOptions, + ): Mock; + /** + * This function is syntax sugar for `MockTracker.method` with `options.getter` set to `true`. + * @since v19.3.0, v18.13.0 + */ + getter< + MockedObject extends object, + MethodName extends keyof MockedObject, + >( + object: MockedObject, + methodName: MethodName, + options?: MockFunctionOptions, + ): Mock<() => MockedObject[MethodName]>; + getter< + MockedObject extends object, + MethodName extends keyof MockedObject, + Implementation extends Function, + >( + object: MockedObject, + methodName: MethodName, + implementation?: Implementation, + options?: MockFunctionOptions, + ): Mock<(() => MockedObject[MethodName]) | Implementation>; + /** + * This function is syntax sugar for `MockTracker.method` with `options.setter` set to `true`. + * @since v19.3.0, v18.13.0 + */ + setter< + MockedObject extends object, + MethodName extends keyof MockedObject, + >( + object: MockedObject, + methodName: MethodName, + options?: MockFunctionOptions, + ): Mock<(value: MockedObject[MethodName]) => void>; + setter< + MockedObject extends object, + MethodName extends keyof MockedObject, + Implementation extends Function, + >( + object: MockedObject, + methodName: MethodName, + implementation?: Implementation, + options?: MockFunctionOptions, + ): Mock<((value: MockedObject[MethodName]) => void) | Implementation>; + /** + * This function is used to mock the exports of ECMAScript modules, CommonJS modules, and Node.js builtin modules. + * Any references to the original module prior to mocking are not impacted. + * + * Only available through the [--experimental-test-module-mocks](https://nodejs.org/api/cli.html#--experimental-test-module-mocks) flag. + * @since v22.3.0 + * @experimental + * @param specifier A string identifying the module to mock. + * @param options Optional configuration options for the mock module. + */ + module(specifier: string | URL, options?: MockModuleOptions): MockModuleContext; + /** + * This function restores the default behavior of all mocks that were previously + * created by this `MockTracker` and disassociates the mocks from the `MockTracker` instance. Once disassociated, the mocks can still be used, but the `MockTracker` instance can no longer be + * used to reset their behavior or + * otherwise interact with them. + * + * After each test completes, this function is called on the test context's `MockTracker`. If the global `MockTracker` is used extensively, calling this + * function manually is recommended. + * @since v19.1.0, v18.13.0 + */ + reset(): void; + /** + * This function restores the default behavior of all mocks that were previously + * created by this `MockTracker`. Unlike `mock.reset()`, `mock.restoreAll()` does + * not disassociate the mocks from the `MockTracker` instance. + * @since v19.1.0, v18.13.0 + */ + restoreAll(): void; + readonly timers: MockTimers; + } + const mock: MockTracker; + interface MockFunctionCall< + F extends Function, + ReturnType = F extends (...args: any) => infer T ? T + : F extends abstract new(...args: any) => infer T ? T + : unknown, + Args = F extends (...args: infer Y) => any ? Y + : F extends abstract new(...args: infer Y) => any ? Y + : unknown[], + > { + /** + * An array of the arguments passed to the mock function. + */ + arguments: Args; + /** + * If the mocked function threw then this property contains the thrown value. + */ + error: unknown | undefined; + /** + * The value returned by the mocked function. + * + * If the mocked function threw, it will be `undefined`. + */ + result: ReturnType | undefined; + /** + * An `Error` object whose stack can be used to determine the callsite of the mocked function invocation. + */ + stack: Error; + /** + * If the mocked function is a constructor, this field contains the class being constructed. + * Otherwise this will be `undefined`. + */ + target: F extends abstract new(...args: any) => any ? F : undefined; + /** + * The mocked function's `this` value. + */ + this: unknown; + } + /** + * The `MockFunctionContext` class is used to inspect or manipulate the behavior of + * mocks created via the `MockTracker` APIs. + * @since v19.1.0, v18.13.0 + */ + interface MockFunctionContext { + /** + * A getter that returns a copy of the internal array used to track calls to the + * mock. Each entry in the array is an object with the following properties. + * @since v19.1.0, v18.13.0 + */ + readonly calls: MockFunctionCall[]; + /** + * This function returns the number of times that this mock has been invoked. This + * function is more efficient than checking `ctx.calls.length` because `ctx.calls` is a getter that creates a copy of the internal call tracking array. + * @since v19.1.0, v18.13.0 + * @return The number of times that this mock has been invoked. + */ + callCount(): number; + /** + * This function is used to change the behavior of an existing mock. + * + * The following example creates a mock function using `t.mock.fn()`, calls the + * mock function, and then changes the mock implementation to a different function. + * + * ```js + * test('changes a mock behavior', (t) => { + * let cnt = 0; + * + * function addOne() { + * cnt++; + * return cnt; + * } + * + * function addTwo() { + * cnt += 2; + * return cnt; + * } + * + * const fn = t.mock.fn(addOne); + * + * assert.strictEqual(fn(), 1); + * fn.mock.mockImplementation(addTwo); + * assert.strictEqual(fn(), 3); + * assert.strictEqual(fn(), 5); + * }); + * ``` + * @since v19.1.0, v18.13.0 + * @param implementation The function to be used as the mock's new implementation. + */ + mockImplementation(implementation: F): void; + /** + * This function is used to change the behavior of an existing mock for a single + * invocation. Once invocation `onCall` has occurred, the mock will revert to + * whatever behavior it would have used had `mockImplementationOnce()` not been + * called. + * + * The following example creates a mock function using `t.mock.fn()`, calls the + * mock function, changes the mock implementation to a different function for the + * next invocation, and then resumes its previous behavior. + * + * ```js + * test('changes a mock behavior once', (t) => { + * let cnt = 0; + * + * function addOne() { + * cnt++; + * return cnt; + * } + * + * function addTwo() { + * cnt += 2; + * return cnt; + * } + * + * const fn = t.mock.fn(addOne); + * + * assert.strictEqual(fn(), 1); + * fn.mock.mockImplementationOnce(addTwo); + * assert.strictEqual(fn(), 3); + * assert.strictEqual(fn(), 4); + * }); + * ``` + * @since v19.1.0, v18.13.0 + * @param implementation The function to be used as the mock's implementation for the invocation number specified by `onCall`. + * @param onCall The invocation number that will use `implementation`. If the specified invocation has already occurred then an exception is thrown. + */ + mockImplementationOnce(implementation: F, onCall?: number): void; + /** + * Resets the call history of the mock function. + * @since v19.3.0, v18.13.0 + */ + resetCalls(): void; + /** + * Resets the implementation of the mock function to its original behavior. The + * mock can still be used after calling this function. + * @since v19.1.0, v18.13.0 + */ + restore(): void; + } + /** + * @since v22.3.0 + * @experimental + */ + interface MockModuleContext { + /** + * Resets the implementation of the mock module. + * @since v22.3.0 + */ + restore(): void; + } + interface MockTimersOptions { + apis: ReadonlyArray<"setInterval" | "setTimeout" | "setImmediate" | "Date">; + now?: number | Date | undefined; + } + /** + * Mocking timers is a technique commonly used in software testing to simulate and + * control the behavior of timers, such as `setInterval` and `setTimeout`, + * without actually waiting for the specified time intervals. + * + * The MockTimers API also allows for mocking of the `Date` constructor and + * `setImmediate`/`clearImmediate` functions. + * + * The `MockTracker` provides a top-level `timers` export + * which is a `MockTimers` instance. + * @since v20.4.0 + * @experimental + */ + interface MockTimers { + /** + * Enables timer mocking for the specified timers. + * + * **Note:** When you enable mocking for a specific timer, its associated + * clear function will also be implicitly mocked. + * + * **Note:** Mocking `Date` will affect the behavior of the mocked timers + * as they use the same internal clock. + * + * Example usage without setting initial time: + * + * ```js + * import { mock } from 'node:test'; + * mock.timers.enable({ apis: ['setInterval', 'Date'], now: 1234 }); + * ``` + * + * The above example enables mocking for the `Date` constructor, `setInterval` timer and + * implicitly mocks the `clearInterval` function. Only the `Date` constructor from `globalThis`, + * `setInterval` and `clearInterval` functions from `node:timers`, `node:timers/promises`, and `globalThis` will be mocked. + * + * Example usage with initial time set + * + * ```js + * import { mock } from 'node:test'; + * mock.timers.enable({ apis: ['Date'], now: 1000 }); + * ``` + * + * Example usage with initial Date object as time set + * + * ```js + * import { mock } from 'node:test'; + * mock.timers.enable({ apis: ['Date'], now: new Date() }); + * ``` + * + * Alternatively, if you call `mock.timers.enable()` without any parameters: + * + * All timers (`'setInterval'`, `'clearInterval'`, `'Date'`, `'setImmediate'`, `'clearImmediate'`, `'setTimeout'`, and `'clearTimeout'`) + * will be mocked. + * + * The `setInterval`, `clearInterval`, `setTimeout`, and `clearTimeout` functions from `node:timers`, `node:timers/promises`, + * and `globalThis` will be mocked. + * The `Date` constructor from `globalThis` will be mocked. + * + * If there is no initial epoch set, the initial date will be based on 0 in the Unix epoch. This is `January 1st, 1970, 00:00:00 UTC`. You can + * set an initial date by passing a now property to the `.enable()` method. This value will be used as the initial date for the mocked Date + * object. It can either be a positive integer, or another Date object. + * @since v20.4.0 + */ + enable(options?: MockTimersOptions): void; + /** + * You can use the `.setTime()` method to manually move the mocked date to another time. This method only accepts a positive integer. + * Note: This method will execute any mocked timers that are in the past from the new time. + * In the below example we are setting a new time for the mocked date. + * ```js + * import assert from 'node:assert'; + * import { test } from 'node:test'; + * test('sets the time of a date object', (context) => { + * // Optionally choose what to mock + * context.mock.timers.enable({ apis: ['Date'], now: 100 }); + * assert.strictEqual(Date.now(), 100); + * // Advance in time will also advance the date + * context.mock.timers.setTime(1000); + * context.mock.timers.tick(200); + * assert.strictEqual(Date.now(), 1200); + * }); + * ``` + */ + setTime(time: number): void; + /** + * This function restores the default behavior of all mocks that were previously + * created by this `MockTimers` instance and disassociates the mocks + * from the `MockTracker` instance. + * + * **Note:** After each test completes, this function is called on + * the test context's `MockTracker`. + * + * ```js + * import { mock } from 'node:test'; + * mock.timers.reset(); + * ``` + * @since v20.4.0 + */ + reset(): void; + /** + * Advances time for all mocked timers. + * + * **Note:** This diverges from how `setTimeout` in Node.js behaves and accepts + * only positive numbers. In Node.js, `setTimeout` with negative numbers is + * only supported for web compatibility reasons. + * + * The following example mocks a `setTimeout` function and + * by using `.tick` advances in + * time triggering all pending timers. + * + * ```js + * import assert from 'node:assert'; + * import { test } from 'node:test'; + * + * test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + * const fn = context.mock.fn(); + * + * context.mock.timers.enable({ apis: ['setTimeout'] }); + * + * setTimeout(fn, 9999); + * + * assert.strictEqual(fn.mock.callCount(), 0); + * + * // Advance in time + * context.mock.timers.tick(9999); + * + * assert.strictEqual(fn.mock.callCount(), 1); + * }); + * ``` + * + * Alternativelly, the `.tick` function can be called many times + * + * ```js + * import assert from 'node:assert'; + * import { test } from 'node:test'; + * + * test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + * const fn = context.mock.fn(); + * context.mock.timers.enable({ apis: ['setTimeout'] }); + * const nineSecs = 9000; + * setTimeout(fn, nineSecs); + * + * const twoSeconds = 3000; + * context.mock.timers.tick(twoSeconds); + * context.mock.timers.tick(twoSeconds); + * context.mock.timers.tick(twoSeconds); + * + * assert.strictEqual(fn.mock.callCount(), 1); + * }); + * ``` + * + * Advancing time using `.tick` will also advance the time for any `Date` object + * created after the mock was enabled (if `Date` was also set to be mocked). + * + * ```js + * import assert from 'node:assert'; + * import { test } from 'node:test'; + * + * test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + * const fn = context.mock.fn(); + * + * context.mock.timers.enable({ apis: ['setTimeout', 'Date'] }); + * setTimeout(fn, 9999); + * + * assert.strictEqual(fn.mock.callCount(), 0); + * assert.strictEqual(Date.now(), 0); + * + * // Advance in time + * context.mock.timers.tick(9999); + * assert.strictEqual(fn.mock.callCount(), 1); + * assert.strictEqual(Date.now(), 9999); + * }); + * ``` + * @since v20.4.0 + */ + tick(milliseconds: number): void; + /** + * Triggers all pending mocked timers immediately. If the `Date` object is also + * mocked, it will also advance the `Date` object to the furthest timer's time. + * + * The example below triggers all pending timers immediately, + * causing them to execute without any delay. + * + * ```js + * import assert from 'node:assert'; + * import { test } from 'node:test'; + * + * test('runAll functions following the given order', (context) => { + * context.mock.timers.enable({ apis: ['setTimeout', 'Date'] }); + * const results = []; + * setTimeout(() => results.push(1), 9999); + * + * // Notice that if both timers have the same timeout, + * // the order of execution is guaranteed + * setTimeout(() => results.push(3), 8888); + * setTimeout(() => results.push(2), 8888); + * + * assert.deepStrictEqual(results, []); + * + * context.mock.timers.runAll(); + * assert.deepStrictEqual(results, [3, 2, 1]); + * // The Date object is also advanced to the furthest timer's time + * assert.strictEqual(Date.now(), 9999); + * }); + * ``` + * + * **Note:** The `runAll()` function is specifically designed for + * triggering timers in the context of timer mocking. + * It does not have any effect on real-time system + * clocks or actual timers outside of the mocking environment. + * @since v20.4.0 + */ + runAll(): void; + /** + * Calls {@link MockTimers.reset()}. + */ + [Symbol.dispose](): void; + } + /** + * An object whose methods are used to configure available assertions on the + * `TestContext` objects in the current process. The methods from `node:assert` + * and snapshot testing functions are available by default. + * + * It is possible to apply the same configuration to all files by placing common + * configuration code in a module + * preloaded with `--require` or `--import`. + * @since v22.14.0 + */ + namespace assert { + /** + * Defines a new assertion function with the provided name and function. If an + * assertion already exists with the same name, it is overwritten. + * @since v22.14.0 + */ + function register(name: string, fn: (this: TestContext, ...args: any[]) => void): void; + } + /** + * @since v22.3.0 + */ + namespace snapshot { + /** + * This function is used to customize the default serialization mechanism used by the test runner. + * + * By default, the test runner performs serialization by calling `JSON.stringify(value, null, 2)` on the provided value. + * `JSON.stringify()` does have limitations regarding circular structures and supported data types. + * If a more robust serialization mechanism is required, this function should be used to specify a list of custom serializers. + * + * Serializers are called in order, with the output of the previous serializer passed as input to the next. + * The final result must be a string value. + * @since v22.3.0 + * @param serializers An array of synchronous functions used as the default serializers for snapshot tests. + */ + function setDefaultSnapshotSerializers(serializers: ReadonlyArray<(value: any) => any>): void; + /** + * This function is used to set a custom resolver for the location of the snapshot file used for snapshot testing. + * By default, the snapshot filename is the same as the entry point filename with `.snapshot` appended. + * @since v22.3.0 + * @param fn A function used to compute the location of the snapshot file. + * The function receives the path of the test file as its only argument. If the + * test is not associated with a file (for example in the REPL), the input is + * undefined. `fn()` must return a string specifying the location of the snapshot file. + */ + function setResolveSnapshotPath(fn: (path: string | undefined) => string): void; + } + } + type FunctionPropertyNames = { + [K in keyof T]: T[K] extends Function ? K : never; + }[keyof T]; + export = test; +} + +/** + * The `node:test/reporters` module exposes the builtin-reporters for `node:test`. + * To access it: + * + * ```js + * import test from 'node:test/reporters'; + * ``` + * + * This module is only available under the `node:` scheme. The following will not + * work: + * + * ```js + * import test from 'node:test/reporters'; + * ``` + * @since v19.9.0 + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/test/reporters.js) + */ +declare module "node:test/reporters" { + import { Transform, TransformOptions } from "node:stream"; + import { EventData } from "node:test"; + + type TestEvent = + | { type: "test:coverage"; data: EventData.TestCoverage } + | { type: "test:complete"; data: EventData.TestComplete } + | { type: "test:dequeue"; data: EventData.TestDequeue } + | { type: "test:diagnostic"; data: EventData.TestDiagnostic } + | { type: "test:enqueue"; data: EventData.TestEnqueue } + | { type: "test:fail"; data: EventData.TestFail } + | { type: "test:pass"; data: EventData.TestPass } + | { type: "test:plan"; data: EventData.TestPlan } + | { type: "test:start"; data: EventData.TestStart } + | { type: "test:stderr"; data: EventData.TestStderr } + | { type: "test:stdout"; data: EventData.TestStdout } + | { type: "test:summary"; data: EventData.TestSummary } + | { type: "test:watch:drained"; data: undefined }; + type TestEventGenerator = AsyncGenerator; + + interface ReporterConstructorWrapper Transform> { + new(...args: ConstructorParameters): InstanceType; + (...args: ConstructorParameters): InstanceType; + } + + /** + * The `dot` reporter outputs the test results in a compact format, + * where each passing test is represented by a `.`, + * and each failing test is represented by a `X`. + * @since v20.0.0 + */ + function dot(source: TestEventGenerator): AsyncGenerator<"\n" | "." | "X", void>; + /** + * The `tap` reporter outputs the test results in the [TAP](https://testanything.org/) format. + * @since v20.0.0 + */ + function tap(source: TestEventGenerator): AsyncGenerator; + class SpecReporter extends Transform { + constructor(); + } + /** + * The `spec` reporter outputs the test results in a human-readable format. + * @since v20.0.0 + */ + const spec: ReporterConstructorWrapper; + /** + * The `junit` reporter outputs test results in a jUnit XML format. + * @since v21.0.0 + */ + function junit(source: TestEventGenerator): AsyncGenerator; + class LcovReporter extends Transform { + constructor(opts?: Omit); + } + /** + * The `lcov` reporter outputs test coverage when used with the + * [`--experimental-test-coverage`](https://nodejs.org/docs/latest-v22.x/api/cli.html#--experimental-test-coverage) flag. + * @since v22.0.0 + */ + const lcov: LcovReporter; + + export { dot, junit, lcov, spec, tap, TestEvent }; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/timers.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/timers.d.ts new file mode 100644 index 00000000..44bc977e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/timers.d.ts @@ -0,0 +1,287 @@ +/** + * The `timer` module exposes a global API for scheduling functions to + * be called at some future period of time. Because the timer functions are + * globals, there is no need to import `node:timers` to use the API. + * + * The timer functions within Node.js implement a similar API as the timers API + * provided by Web Browsers but use a different internal implementation that is + * built around the Node.js [Event Loop](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout). + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/timers.js) + */ +declare module "timers" { + import { Abortable } from "node:events"; + import * as promises from "node:timers/promises"; + export interface TimerOptions extends Abortable { + /** + * Set to `false` to indicate that the scheduled `Timeout` + * should not require the Node.js event loop to remain active. + * @default true + */ + ref?: boolean | undefined; + } + global { + namespace NodeJS { + /** + * This object is created internally and is returned from `setImmediate()`. It + * can be passed to `clearImmediate()` in order to cancel the scheduled + * actions. + * + * By default, when an immediate is scheduled, the Node.js event loop will continue + * running as long as the immediate is active. The `Immediate` object returned by + * `setImmediate()` exports both `immediate.ref()` and `immediate.unref()` + * functions that can be used to control this default behavior. + */ + interface Immediate extends RefCounted, Disposable { + /** + * If true, the `Immediate` object will keep the Node.js event loop active. + * @since v11.0.0 + */ + hasRef(): boolean; + /** + * When called, requests that the Node.js event loop _not_ exit so long as the + * `Immediate` is active. Calling `immediate.ref()` multiple times will have no + * effect. + * + * By default, all `Immediate` objects are "ref'ed", making it normally unnecessary + * to call `immediate.ref()` unless `immediate.unref()` had been called previously. + * @since v9.7.0 + * @returns a reference to `immediate` + */ + ref(): this; + /** + * When called, the active `Immediate` object will not require the Node.js event + * loop to remain active. If there is no other activity keeping the event loop + * running, the process may exit before the `Immediate` object's callback is + * invoked. Calling `immediate.unref()` multiple times will have no effect. + * @since v9.7.0 + * @returns a reference to `immediate` + */ + unref(): this; + /** + * Cancels the immediate. This is similar to calling `clearImmediate()`. + * @since v20.5.0, v18.18.0 + * @experimental + */ + [Symbol.dispose](): void; + _onImmediate(...args: any[]): void; + } + // Legacy interface used in Node.js v9 and prior + // TODO: remove in a future major version bump + /** @deprecated Use `NodeJS.Timeout` instead. */ + interface Timer extends RefCounted { + hasRef(): boolean; + refresh(): this; + [Symbol.toPrimitive](): number; + } + /** + * This object is created internally and is returned from `setTimeout()` and + * `setInterval()`. It can be passed to either `clearTimeout()` or + * `clearInterval()` in order to cancel the scheduled actions. + * + * By default, when a timer is scheduled using either `setTimeout()` or + * `setInterval()`, the Node.js event loop will continue running as long as the + * timer is active. Each of the `Timeout` objects returned by these functions + * export both `timeout.ref()` and `timeout.unref()` functions that can be used to + * control this default behavior. + */ + interface Timeout extends RefCounted, Disposable, Timer { + /** + * Cancels the timeout. + * @since v0.9.1 + * @legacy Use `clearTimeout()` instead. + * @returns a reference to `timeout` + */ + close(): this; + /** + * If true, the `Timeout` object will keep the Node.js event loop active. + * @since v11.0.0 + */ + hasRef(): boolean; + /** + * When called, requests that the Node.js event loop _not_ exit so long as the + * `Timeout` is active. Calling `timeout.ref()` multiple times will have no effect. + * + * By default, all `Timeout` objects are "ref'ed", making it normally unnecessary + * to call `timeout.ref()` unless `timeout.unref()` had been called previously. + * @since v0.9.1 + * @returns a reference to `timeout` + */ + ref(): this; + /** + * Sets the timer's start time to the current time, and reschedules the timer to + * call its callback at the previously specified duration adjusted to the current + * time. This is useful for refreshing a timer without allocating a new + * JavaScript object. + * + * Using this on a timer that has already called its callback will reactivate the + * timer. + * @since v10.2.0 + * @returns a reference to `timeout` + */ + refresh(): this; + /** + * When called, the active `Timeout` object will not require the Node.js event loop + * to remain active. If there is no other activity keeping the event loop running, + * the process may exit before the `Timeout` object's callback is invoked. Calling + * `timeout.unref()` multiple times will have no effect. + * @since v0.9.1 + * @returns a reference to `timeout` + */ + unref(): this; + /** + * Coerce a `Timeout` to a primitive. The primitive can be used to + * clear the `Timeout`. The primitive can only be used in the + * same thread where the timeout was created. Therefore, to use it + * across `worker_threads` it must first be passed to the correct + * thread. This allows enhanced compatibility with browser + * `setTimeout()` and `setInterval()` implementations. + * @since v14.9.0, v12.19.0 + */ + [Symbol.toPrimitive](): number; + /** + * Cancels the timeout. + * @since v20.5.0, v18.18.0 + * @experimental + */ + [Symbol.dispose](): void; + _onTimeout(...args: any[]): void; + } + } + /** + * Schedules the "immediate" execution of the `callback` after I/O events' + * callbacks. + * + * When multiple calls to `setImmediate()` are made, the `callback` functions are + * queued for execution in the order in which they are created. The entire callback + * queue is processed every event loop iteration. If an immediate timer is queued + * from inside an executing callback, that timer will not be triggered until the + * next event loop iteration. + * + * If `callback` is not a function, a `TypeError` will be thrown. + * + * This method has a custom variant for promises that is available using + * `timersPromises.setImmediate()`. + * @since v0.9.1 + * @param callback The function to call at the end of this turn of + * the Node.js [Event Loop](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout) + * @param args Optional arguments to pass when the `callback` is called. + * @returns for use with `clearImmediate()` + */ + function setImmediate( + callback: (...args: TArgs) => void, + ...args: TArgs + ): NodeJS.Immediate; + // Allow a single void-accepting argument to be optional in arguments lists. + // Allows usage such as `new Promise(resolve => setTimeout(resolve, ms))` (#54258) + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + function setImmediate(callback: (_: void) => void): NodeJS.Immediate; + namespace setImmediate { + import __promisify__ = promises.setImmediate; + export { __promisify__ }; + } + /** + * Schedules repeated execution of `callback` every `delay` milliseconds. + * + * When `delay` is larger than `2147483647` or less than `1` or `NaN`, the `delay` + * will be set to `1`. Non-integer delays are truncated to an integer. + * + * If `callback` is not a function, a `TypeError` will be thrown. + * + * This method has a custom variant for promises that is available using + * `timersPromises.setInterval()`. + * @since v0.0.1 + * @param callback The function to call when the timer elapses. + * @param delay The number of milliseconds to wait before calling the + * `callback`. **Default:** `1`. + * @param args Optional arguments to pass when the `callback` is called. + * @returns for use with `clearInterval()` + */ + function setInterval( + callback: (...args: TArgs) => void, + delay?: number, + ...args: TArgs + ): NodeJS.Timeout; + // Allow a single void-accepting argument to be optional in arguments lists. + // Allows usage such as `new Promise(resolve => setTimeout(resolve, ms))` (#54258) + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + function setInterval(callback: (_: void) => void, delay?: number): NodeJS.Timeout; + /** + * Schedules execution of a one-time `callback` after `delay` milliseconds. + * + * The `callback` will likely not be invoked in precisely `delay` milliseconds. + * Node.js makes no guarantees about the exact timing of when callbacks will fire, + * nor of their ordering. The callback will be called as close as possible to the + * time specified. + * + * When `delay` is larger than `2147483647` or less than `1` or `NaN`, the `delay` + * will be set to `1`. Non-integer delays are truncated to an integer. + * + * If `callback` is not a function, a `TypeError` will be thrown. + * + * This method has a custom variant for promises that is available using + * `timersPromises.setTimeout()`. + * @since v0.0.1 + * @param callback The function to call when the timer elapses. + * @param delay The number of milliseconds to wait before calling the + * `callback`. **Default:** `1`. + * @param args Optional arguments to pass when the `callback` is called. + * @returns for use with `clearTimeout()` + */ + function setTimeout( + callback: (...args: TArgs) => void, + delay?: number, + ...args: TArgs + ): NodeJS.Timeout; + // Allow a single void-accepting argument to be optional in arguments lists. + // Allows usage such as `new Promise(resolve => setTimeout(resolve, ms))` (#54258) + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout; + namespace setTimeout { + import __promisify__ = promises.setTimeout; + export { __promisify__ }; + } + /** + * Cancels an `Immediate` object created by `setImmediate()`. + * @since v0.9.1 + * @param immediate An `Immediate` object as returned by `setImmediate()`. + */ + function clearImmediate(immediate: NodeJS.Immediate | undefined): void; + /** + * Cancels a `Timeout` object created by `setInterval()`. + * @since v0.0.1 + * @param timeout A `Timeout` object as returned by `setInterval()` + * or the primitive of the `Timeout` object as a string or a number. + */ + function clearInterval(timeout: NodeJS.Timeout | string | number | undefined): void; + /** + * Cancels a `Timeout` object created by `setTimeout()`. + * @since v0.0.1 + * @param timeout A `Timeout` object as returned by `setTimeout()` + * or the primitive of the `Timeout` object as a string or a number. + */ + function clearTimeout(timeout: NodeJS.Timeout | string | number | undefined): void; + /** + * The `queueMicrotask()` method queues a microtask to invoke `callback`. If + * `callback` throws an exception, the `process` object `'uncaughtException'` + * event will be emitted. + * + * The microtask queue is managed by V8 and may be used in a similar manner to + * the `process.nextTick()` queue, which is managed by Node.js. The + * `process.nextTick()` queue is always processed before the microtask queue + * within each turn of the Node.js event loop. + * @since v11.0.0 + * @param callback Function to be queued. + */ + function queueMicrotask(callback: () => void): void; + } + import clearImmediate = globalThis.clearImmediate; + import clearInterval = globalThis.clearInterval; + import clearTimeout = globalThis.clearTimeout; + import setImmediate = globalThis.setImmediate; + import setInterval = globalThis.setInterval; + import setTimeout = globalThis.setTimeout; + export { clearImmediate, clearInterval, clearTimeout, promises, setImmediate, setInterval, setTimeout }; +} +declare module "node:timers" { + export * from "timers"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/timers/promises.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/timers/promises.d.ts new file mode 100644 index 00000000..05db90c6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/timers/promises.d.ts @@ -0,0 +1,108 @@ +/** + * The `timers/promises` API provides an alternative set of timer functions + * that return `Promise` objects. The API is accessible via + * `require('node:timers/promises')`. + * + * ```js + * import { + * setTimeout, + * setImmediate, + * setInterval, + * } from 'node:timers/promises'; + * ``` + * @since v15.0.0 + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/timers/promises.js) + */ +declare module "timers/promises" { + import { TimerOptions } from "node:timers"; + /** + * ```js + * import { + * setTimeout, + * } from 'node:timers/promises'; + * + * const res = await setTimeout(100, 'result'); + * + * console.log(res); // Prints 'result' + * ``` + * @since v15.0.0 + * @param delay The number of milliseconds to wait before fulfilling the + * promise. **Default:** `1`. + * @param value A value with which the promise is fulfilled. + */ + function setTimeout(delay?: number, value?: T, options?: TimerOptions): Promise; + /** + * ```js + * import { + * setImmediate, + * } from 'node:timers/promises'; + * + * const res = await setImmediate('result'); + * + * console.log(res); // Prints 'result' + * ``` + * @since v15.0.0 + * @param value A value with which the promise is fulfilled. + */ + function setImmediate(value?: T, options?: TimerOptions): Promise; + /** + * Returns an async iterator that generates values in an interval of `delay` ms. + * If `ref` is `true`, you need to call `next()` of async iterator explicitly + * or implicitly to keep the event loop alive. + * + * ```js + * import { + * setInterval, + * } from 'node:timers/promises'; + * + * const interval = 100; + * for await (const startTime of setInterval(interval, Date.now())) { + * const now = Date.now(); + * console.log(now); + * if ((now - startTime) > 1000) + * break; + * } + * console.log(Date.now()); + * ``` + * @since v15.9.0 + * @param delay The number of milliseconds to wait between iterations. + * **Default:** `1`. + * @param value A value with which the iterator returns. + */ + function setInterval(delay?: number, value?: T, options?: TimerOptions): NodeJS.AsyncIterator; + interface Scheduler { + /** + * An experimental API defined by the [Scheduling APIs](https://github.com/WICG/scheduling-apis) draft specification + * being developed as a standard Web Platform API. + * + * Calling `timersPromises.scheduler.wait(delay, options)` is roughly equivalent + * to calling `timersPromises.setTimeout(delay, undefined, options)` except that + * the `ref` option is not supported. + * + * ```js + * import { scheduler } from 'node:timers/promises'; + * + * await scheduler.wait(1000); // Wait one second before continuing + * ``` + * @since v17.3.0, v16.14.0 + * @experimental + * @param delay The number of milliseconds to wait before resolving the + * promise. + */ + wait(delay: number, options?: { signal?: AbortSignal }): Promise; + /** + * An experimental API defined by the [Scheduling APIs](https://github.com/WICG/scheduling-apis) draft specification + * being developed as a standard Web Platform API. + * + * Calling `timersPromises.scheduler.yield()` is equivalent to calling + * `timersPromises.setImmediate()` with no arguments. + * @since v17.3.0, v16.14.0 + * @experimental + */ + yield(): Promise; + } + const scheduler: Scheduler; +} +declare module "node:timers/promises" { + export * from "timers/promises"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/tls.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/tls.d.ts new file mode 100644 index 00000000..51770320 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/tls.d.ts @@ -0,0 +1,1319 @@ +/** + * The `node:tls` module provides an implementation of the Transport Layer Security + * (TLS) and Secure Socket Layer (SSL) protocols that is built on top of OpenSSL. + * The module can be accessed using: + * + * ```js + * import tls from 'node:tls'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/tls.js) + */ +declare module "tls" { + import { NonSharedBuffer } from "node:buffer"; + import { X509Certificate } from "node:crypto"; + import * as net from "node:net"; + import * as stream from "stream"; + const CLIENT_RENEG_LIMIT: number; + const CLIENT_RENEG_WINDOW: number; + interface Certificate extends NodeJS.Dict { + /** + * Country code. + */ + C?: string | string[]; + /** + * Street. + */ + ST?: string | string[]; + /** + * Locality. + */ + L?: string | string[]; + /** + * Organization. + */ + O?: string | string[]; + /** + * Organizational unit. + */ + OU?: string | string[]; + /** + * Common name. + */ + CN?: string | string[]; + } + interface PeerCertificate { + /** + * `true` if a Certificate Authority (CA), `false` otherwise. + * @since v18.13.0 + */ + ca: boolean; + /** + * The DER encoded X.509 certificate data. + */ + raw: NonSharedBuffer; + /** + * The certificate subject. + */ + subject: Certificate; + /** + * The certificate issuer, described in the same terms as the `subject`. + */ + issuer: Certificate; + /** + * The date-time the certificate is valid from. + */ + valid_from: string; + /** + * The date-time the certificate is valid to. + */ + valid_to: string; + /** + * The certificate serial number, as a hex string. + */ + serialNumber: string; + /** + * The SHA-1 digest of the DER encoded certificate. + * It is returned as a `:` separated hexadecimal string. + */ + fingerprint: string; + /** + * The SHA-256 digest of the DER encoded certificate. + * It is returned as a `:` separated hexadecimal string. + */ + fingerprint256: string; + /** + * The SHA-512 digest of the DER encoded certificate. + * It is returned as a `:` separated hexadecimal string. + */ + fingerprint512: string; + /** + * The extended key usage, a set of OIDs. + */ + ext_key_usage?: string[]; + /** + * A string containing concatenated names for the subject, + * an alternative to the `subject` names. + */ + subjectaltname?: string; + /** + * An array describing the AuthorityInfoAccess, used with OCSP. + */ + infoAccess?: NodeJS.Dict; + /** + * For RSA keys: The RSA bit size. + * + * For EC keys: The key size in bits. + */ + bits?: number; + /** + * The RSA exponent, as a string in hexadecimal number notation. + */ + exponent?: string; + /** + * The RSA modulus, as a hexadecimal string. + */ + modulus?: string; + /** + * The public key. + */ + pubkey?: NonSharedBuffer; + /** + * The ASN.1 name of the OID of the elliptic curve. + * Well-known curves are identified by an OID. + * While it is unusual, it is possible that the curve + * is identified by its mathematical properties, + * in which case it will not have an OID. + */ + asn1Curve?: string; + /** + * The NIST name for the elliptic curve, if it has one + * (not all well-known curves have been assigned names by NIST). + */ + nistCurve?: string; + } + interface DetailedPeerCertificate extends PeerCertificate { + /** + * The issuer certificate object. + * For self-signed certificates, this may be a circular reference. + */ + issuerCertificate: DetailedPeerCertificate; + } + interface CipherNameAndProtocol { + /** + * The cipher name. + */ + name: string; + /** + * SSL/TLS protocol version. + */ + version: string; + /** + * IETF name for the cipher suite. + */ + standardName: string; + } + interface EphemeralKeyInfo { + /** + * The supported types are 'DH' and 'ECDH'. + */ + type: string; + /** + * The name property is available only when type is 'ECDH'. + */ + name?: string | undefined; + /** + * The size of parameter of an ephemeral key exchange. + */ + size: number; + } + interface KeyObject { + /** + * Private keys in PEM format. + */ + pem: string | Buffer; + /** + * Optional passphrase. + */ + passphrase?: string | undefined; + } + interface PxfObject { + /** + * PFX or PKCS12 encoded private key and certificate chain. + */ + buf: string | Buffer; + /** + * Optional passphrase. + */ + passphrase?: string | undefined; + } + interface TLSSocketOptions extends SecureContextOptions, CommonConnectionOptions { + /** + * If true the TLS socket will be instantiated in server-mode. + * Defaults to false. + */ + isServer?: boolean | undefined; + /** + * An optional net.Server instance. + */ + server?: net.Server | undefined; + /** + * An optional Buffer instance containing a TLS session. + */ + session?: Buffer | undefined; + /** + * If true, specifies that the OCSP status request extension will be + * added to the client hello and an 'OCSPResponse' event will be + * emitted on the socket before establishing a secure communication + */ + requestOCSP?: boolean | undefined; + } + /** + * Performs transparent encryption of written data and all required TLS + * negotiation. + * + * Instances of `tls.TLSSocket` implement the duplex `Stream` interface. + * + * Methods that return TLS connection metadata (e.g.{@link TLSSocket.getPeerCertificate}) will only return data while the + * connection is open. + * @since v0.11.4 + */ + class TLSSocket extends net.Socket { + /** + * Construct a new tls.TLSSocket object from an existing TCP socket. + */ + constructor(socket: net.Socket | stream.Duplex, options?: TLSSocketOptions); + /** + * This property is `true` if the peer certificate was signed by one of the CAs + * specified when creating the `tls.TLSSocket` instance, otherwise `false`. + * @since v0.11.4 + */ + authorized: boolean; + /** + * Returns the reason why the peer's certificate was not been verified. This + * property is set only when `tlsSocket.authorized === false`. + * @since v0.11.4 + */ + authorizationError: Error; + /** + * Always returns `true`. This may be used to distinguish TLS sockets from regular`net.Socket` instances. + * @since v0.11.4 + */ + encrypted: true; + /** + * String containing the selected ALPN protocol. + * Before a handshake has completed, this value is always null. + * When a handshake is completed but not ALPN protocol was selected, tlsSocket.alpnProtocol equals false. + */ + alpnProtocol: string | false | null; + /** + * String containing the server name requested via SNI (Server Name Indication) TLS extension. + */ + servername: string | false | null; + /** + * Returns an object representing the local certificate. The returned object has + * some properties corresponding to the fields of the certificate. + * + * See {@link TLSSocket.getPeerCertificate} for an example of the certificate + * structure. + * + * If there is no local certificate, an empty object will be returned. If the + * socket has been destroyed, `null` will be returned. + * @since v11.2.0 + */ + getCertificate(): PeerCertificate | object | null; + /** + * Returns an object containing information on the negotiated cipher suite. + * + * For example, a TLSv1.2 protocol with AES256-SHA cipher: + * + * ```json + * { + * "name": "AES256-SHA", + * "standardName": "TLS_RSA_WITH_AES_256_CBC_SHA", + * "version": "SSLv3" + * } + * ``` + * + * See [SSL\_CIPHER\_get\_name](https://www.openssl.org/docs/man1.1.1/man3/SSL_CIPHER_get_name.html) for more information. + * @since v0.11.4 + */ + getCipher(): CipherNameAndProtocol; + /** + * Returns an object representing the type, name, and size of parameter of + * an ephemeral key exchange in `perfect forward secrecy` on a client + * connection. It returns an empty object when the key exchange is not + * ephemeral. As this is only supported on a client socket; `null` is returned + * if called on a server socket. The supported types are `'DH'` and `'ECDH'`. The `name` property is available only when type is `'ECDH'`. + * + * For example: `{ type: 'ECDH', name: 'prime256v1', size: 256 }`. + * @since v5.0.0 + */ + getEphemeralKeyInfo(): EphemeralKeyInfo | object | null; + /** + * As the `Finished` messages are message digests of the complete handshake + * (with a total of 192 bits for TLS 1.0 and more for SSL 3.0), they can + * be used for external authentication procedures when the authentication + * provided by SSL/TLS is not desired or is not enough. + * + * Corresponds to the `SSL_get_finished` routine in OpenSSL and may be used + * to implement the `tls-unique` channel binding from [RFC 5929](https://tools.ietf.org/html/rfc5929). + * @since v9.9.0 + * @return The latest `Finished` message that has been sent to the socket as part of a SSL/TLS handshake, or `undefined` if no `Finished` message has been sent yet. + */ + getFinished(): NonSharedBuffer | undefined; + /** + * Returns an object representing the peer's certificate. If the peer does not + * provide a certificate, an empty object will be returned. If the socket has been + * destroyed, `null` will be returned. + * + * If the full certificate chain was requested, each certificate will include an`issuerCertificate` property containing an object representing its issuer's + * certificate. + * @since v0.11.4 + * @param detailed Include the full certificate chain if `true`, otherwise include just the peer's certificate. + * @return A certificate object. + */ + getPeerCertificate(detailed: true): DetailedPeerCertificate; + getPeerCertificate(detailed?: false): PeerCertificate; + getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate; + /** + * As the `Finished` messages are message digests of the complete handshake + * (with a total of 192 bits for TLS 1.0 and more for SSL 3.0), they can + * be used for external authentication procedures when the authentication + * provided by SSL/TLS is not desired or is not enough. + * + * Corresponds to the `SSL_get_peer_finished` routine in OpenSSL and may be used + * to implement the `tls-unique` channel binding from [RFC 5929](https://tools.ietf.org/html/rfc5929). + * @since v9.9.0 + * @return The latest `Finished` message that is expected or has actually been received from the socket as part of a SSL/TLS handshake, or `undefined` if there is no `Finished` message so + * far. + */ + getPeerFinished(): NonSharedBuffer | undefined; + /** + * Returns a string containing the negotiated SSL/TLS protocol version of the + * current connection. The value `'unknown'` will be returned for connected + * sockets that have not completed the handshaking process. The value `null` will + * be returned for server sockets or disconnected client sockets. + * + * Protocol versions are: + * + * * `'SSLv3'` + * * `'TLSv1'` + * * `'TLSv1.1'` + * * `'TLSv1.2'` + * * `'TLSv1.3'` + * + * See the OpenSSL [`SSL_get_version`](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html) documentation for more information. + * @since v5.7.0 + */ + getProtocol(): string | null; + /** + * Returns the TLS session data or `undefined` if no session was + * negotiated. On the client, the data can be provided to the `session` option of {@link connect} to resume the connection. On the server, it may be useful + * for debugging. + * + * See `Session Resumption` for more information. + * + * Note: `getSession()` works only for TLSv1.2 and below. For TLSv1.3, applications + * must use the `'session'` event (it also works for TLSv1.2 and below). + * @since v0.11.4 + */ + getSession(): NonSharedBuffer | undefined; + /** + * See [SSL\_get\_shared\_sigalgs](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_shared_sigalgs.html) for more information. + * @since v12.11.0 + * @return List of signature algorithms shared between the server and the client in the order of decreasing preference. + */ + getSharedSigalgs(): string[]; + /** + * For a client, returns the TLS session ticket if one is available, or`undefined`. For a server, always returns `undefined`. + * + * It may be useful for debugging. + * + * See `Session Resumption` for more information. + * @since v0.11.4 + */ + getTLSTicket(): NonSharedBuffer | undefined; + /** + * See `Session Resumption` for more information. + * @since v0.5.6 + * @return `true` if the session was reused, `false` otherwise. + */ + isSessionReused(): boolean; + /** + * The `tlsSocket.renegotiate()` method initiates a TLS renegotiation process. + * Upon completion, the `callback` function will be passed a single argument + * that is either an `Error` (if the request failed) or `null`. + * + * This method can be used to request a peer's certificate after the secure + * connection has been established. + * + * When running as the server, the socket will be destroyed with an error after `handshakeTimeout` timeout. + * + * For TLSv1.3, renegotiation cannot be initiated, it is not supported by the + * protocol. + * @since v0.11.8 + * @param callback If `renegotiate()` returned `true`, callback is attached once to the `'secure'` event. If `renegotiate()` returned `false`, `callback` will be called in the next tick with + * an error, unless the `tlsSocket` has been destroyed, in which case `callback` will not be called at all. + * @return `true` if renegotiation was initiated, `false` otherwise. + */ + renegotiate( + options: { + rejectUnauthorized?: boolean | undefined; + requestCert?: boolean | undefined; + }, + callback: (err: Error | null) => void, + ): undefined | boolean; + /** + * The `tlsSocket.setKeyCert()` method sets the private key and certificate to use for the socket. + * This is mainly useful if you wish to select a server certificate from a TLS server's `ALPNCallback`. + * @since v22.5.0, v20.17.0 + * @param context An object containing at least `key` and `cert` properties from the {@link createSecureContext()} `options`, + * or a TLS context object created with {@link createSecureContext()} itself. + */ + setKeyCert(context: SecureContextOptions | SecureContext): void; + /** + * The `tlsSocket.setMaxSendFragment()` method sets the maximum TLS fragment size. + * Returns `true` if setting the limit succeeded; `false` otherwise. + * + * Smaller fragment sizes decrease the buffering latency on the client: larger + * fragments are buffered by the TLS layer until the entire fragment is received + * and its integrity is verified; large fragments can span multiple roundtrips + * and their processing can be delayed due to packet loss or reordering. However, + * smaller fragments add extra TLS framing bytes and CPU overhead, which may + * decrease overall server throughput. + * @since v0.11.11 + * @param [size=16384] The maximum TLS fragment size. The maximum value is `16384`. + */ + setMaxSendFragment(size: number): boolean; + /** + * Disables TLS renegotiation for this `TLSSocket` instance. Once called, attempts + * to renegotiate will trigger an `'error'` event on the `TLSSocket`. + * @since v8.4.0 + */ + disableRenegotiation(): void; + /** + * When enabled, TLS packet trace information is written to `stderr`. This can be + * used to debug TLS connection problems. + * + * The format of the output is identical to the output of`openssl s_client -trace` or `openssl s_server -trace`. While it is produced by + * OpenSSL's `SSL_trace()` function, the format is undocumented, can change + * without notice, and should not be relied on. + * @since v12.2.0 + */ + enableTrace(): void; + /** + * Returns the peer certificate as an `X509Certificate` object. + * + * If there is no peer certificate, or the socket has been destroyed,`undefined` will be returned. + * @since v15.9.0 + */ + getPeerX509Certificate(): X509Certificate | undefined; + /** + * Returns the local certificate as an `X509Certificate` object. + * + * If there is no local certificate, or the socket has been destroyed,`undefined` will be returned. + * @since v15.9.0 + */ + getX509Certificate(): X509Certificate | undefined; + /** + * Keying material is used for validations to prevent different kind of attacks in + * network protocols, for example in the specifications of IEEE 802.1X. + * + * Example + * + * ```js + * const keyingMaterial = tlsSocket.exportKeyingMaterial( + * 128, + * 'client finished'); + * + * /* + * Example return value of keyingMaterial: + * + * + * ``` + * + * See the OpenSSL [`SSL_export_keying_material`](https://www.openssl.org/docs/man1.1.1/man3/SSL_export_keying_material.html) documentation for more + * information. + * @since v13.10.0, v12.17.0 + * @param length number of bytes to retrieve from keying material + * @param label an application specific label, typically this will be a value from the [IANA Exporter Label + * Registry](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels). + * @param context Optionally provide a context. + * @return requested bytes of the keying material + */ + exportKeyingMaterial(length: number, label: string, context: Buffer): NonSharedBuffer; + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "OCSPResponse", listener: (response: NonSharedBuffer) => void): this; + addListener(event: "secureConnect", listener: () => void): this; + addListener(event: "session", listener: (session: NonSharedBuffer) => void): this; + addListener(event: "keylog", listener: (line: NonSharedBuffer) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "OCSPResponse", response: NonSharedBuffer): boolean; + emit(event: "secureConnect"): boolean; + emit(event: "session", session: NonSharedBuffer): boolean; + emit(event: "keylog", line: NonSharedBuffer): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "OCSPResponse", listener: (response: NonSharedBuffer) => void): this; + on(event: "secureConnect", listener: () => void): this; + on(event: "session", listener: (session: NonSharedBuffer) => void): this; + on(event: "keylog", listener: (line: NonSharedBuffer) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "OCSPResponse", listener: (response: NonSharedBuffer) => void): this; + once(event: "secureConnect", listener: () => void): this; + once(event: "session", listener: (session: NonSharedBuffer) => void): this; + once(event: "keylog", listener: (line: NonSharedBuffer) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "OCSPResponse", listener: (response: NonSharedBuffer) => void): this; + prependListener(event: "secureConnect", listener: () => void): this; + prependListener(event: "session", listener: (session: NonSharedBuffer) => void): this; + prependListener(event: "keylog", listener: (line: NonSharedBuffer) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "OCSPResponse", listener: (response: NonSharedBuffer) => void): this; + prependOnceListener(event: "secureConnect", listener: () => void): this; + prependOnceListener(event: "session", listener: (session: NonSharedBuffer) => void): this; + prependOnceListener(event: "keylog", listener: (line: NonSharedBuffer) => void): this; + } + interface CommonConnectionOptions { + /** + * An optional TLS context object from tls.createSecureContext() + */ + secureContext?: SecureContext | undefined; + /** + * When enabled, TLS packet trace information is written to `stderr`. This can be + * used to debug TLS connection problems. + * @default false + */ + enableTrace?: boolean | undefined; + /** + * If true the server will request a certificate from clients that + * connect and attempt to verify that certificate. Defaults to + * false. + */ + requestCert?: boolean | undefined; + /** + * An array of strings or a Buffer naming possible ALPN protocols. + * (Protocols should be ordered by their priority.) + */ + ALPNProtocols?: readonly string[] | NodeJS.ArrayBufferView | undefined; + /** + * SNICallback(servername, cb) A function that will be + * called if the client supports SNI TLS extension. Two arguments + * will be passed when called: servername and cb. SNICallback should + * invoke cb(null, ctx), where ctx is a SecureContext instance. + * (tls.createSecureContext(...) can be used to get a proper + * SecureContext.) If SNICallback wasn't provided the default callback + * with high-level API will be used (see below). + */ + SNICallback?: ((servername: string, cb: (err: Error | null, ctx?: SecureContext) => void) => void) | undefined; + /** + * If true the server will reject any connection which is not + * authorized with the list of supplied CAs. This option only has an + * effect if requestCert is true. + * @default true + */ + rejectUnauthorized?: boolean | undefined; + } + interface TlsOptions extends SecureContextOptions, CommonConnectionOptions, net.ServerOpts { + /** + * Abort the connection if the SSL/TLS handshake does not finish in the + * specified number of milliseconds. A 'tlsClientError' is emitted on + * the tls.Server object whenever a handshake times out. Default: + * 120000 (120 seconds). + */ + handshakeTimeout?: number | undefined; + /** + * The number of seconds after which a TLS session created by the + * server will no longer be resumable. See Session Resumption for more + * information. Default: 300. + */ + sessionTimeout?: number | undefined; + /** + * 48-bytes of cryptographically strong pseudo-random data. + */ + ticketKeys?: Buffer | undefined; + /** + * @param socket + * @param identity identity parameter sent from the client. + * @return pre-shared key that must either be + * a buffer or `null` to stop the negotiation process. Returned PSK must be + * compatible with the selected cipher's digest. + * + * When negotiating TLS-PSK (pre-shared keys), this function is called + * with the identity provided by the client. + * If the return value is `null` the negotiation process will stop and an + * "unknown_psk_identity" alert message will be sent to the other party. + * If the server wishes to hide the fact that the PSK identity was not known, + * the callback must provide some random data as `psk` to make the connection + * fail with "decrypt_error" before negotiation is finished. + * PSK ciphers are disabled by default, and using TLS-PSK thus + * requires explicitly specifying a cipher suite with the `ciphers` option. + * More information can be found in the RFC 4279. + */ + pskCallback?: ((socket: TLSSocket, identity: string) => NodeJS.ArrayBufferView | null) | undefined; + /** + * hint to send to a client to help + * with selecting the identity during TLS-PSK negotiation. Will be ignored + * in TLS 1.3. Upon failing to set pskIdentityHint `tlsClientError` will be + * emitted with `ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED` code. + */ + pskIdentityHint?: string | undefined; + } + interface PSKCallbackNegotation { + psk: NodeJS.ArrayBufferView; + identity: string; + } + interface ConnectionOptions extends SecureContextOptions, CommonConnectionOptions { + host?: string | undefined; + port?: number | undefined; + path?: string | undefined; // Creates unix socket connection to path. If this option is specified, `host` and `port` are ignored. + socket?: stream.Duplex | undefined; // Establish secure connection on a given socket rather than creating a new socket + checkServerIdentity?: typeof checkServerIdentity | undefined; + servername?: string | undefined; // SNI TLS Extension + session?: Buffer | undefined; + minDHSize?: number | undefined; + lookup?: net.LookupFunction | undefined; + timeout?: number | undefined; + /** + * When negotiating TLS-PSK (pre-shared keys), this function is called + * with optional identity `hint` provided by the server or `null` + * in case of TLS 1.3 where `hint` was removed. + * It will be necessary to provide a custom `tls.checkServerIdentity()` + * for the connection as the default one will try to check hostname/IP + * of the server against the certificate but that's not applicable for PSK + * because there won't be a certificate present. + * More information can be found in the RFC 4279. + * + * @param hint message sent from the server to help client + * decide which identity to use during negotiation. + * Always `null` if TLS 1.3 is used. + * @returns Return `null` to stop the negotiation process. `psk` must be + * compatible with the selected cipher's digest. + * `identity` must use UTF-8 encoding. + */ + pskCallback?: ((hint: string | null) => PSKCallbackNegotation | null) | undefined; + } + /** + * Accepts encrypted connections using TLS or SSL. + * @since v0.3.2 + */ + class Server extends net.Server { + constructor(secureConnectionListener?: (socket: TLSSocket) => void); + constructor(options: TlsOptions, secureConnectionListener?: (socket: TLSSocket) => void); + /** + * The `server.addContext()` method adds a secure context that will be used if + * the client request's SNI name matches the supplied `hostname` (or wildcard). + * + * When there are multiple matching contexts, the most recently added one is + * used. + * @since v0.5.3 + * @param hostname A SNI host name or wildcard (e.g. `'*'`) + * @param context An object containing any of the possible properties from the {@link createSecureContext} `options` arguments (e.g. `key`, `cert`, `ca`, etc), or a TLS context object created + * with {@link createSecureContext} itself. + */ + addContext(hostname: string, context: SecureContextOptions | SecureContext): void; + /** + * Returns the session ticket keys. + * + * See `Session Resumption` for more information. + * @since v3.0.0 + * @return A 48-byte buffer containing the session ticket keys. + */ + getTicketKeys(): NonSharedBuffer; + /** + * The `server.setSecureContext()` method replaces the secure context of an + * existing server. Existing connections to the server are not interrupted. + * @since v11.0.0 + * @param options An object containing any of the possible properties from the {@link createSecureContext} `options` arguments (e.g. `key`, `cert`, `ca`, etc). + */ + setSecureContext(options: SecureContextOptions): void; + /** + * Sets the session ticket keys. + * + * Changes to the ticket keys are effective only for future server connections. + * Existing or currently pending server connections will use the previous keys. + * + * See `Session Resumption` for more information. + * @since v3.0.0 + * @param keys A 48-byte buffer containing the session ticket keys. + */ + setTicketKeys(keys: Buffer): void; + /** + * events.EventEmitter + * 1. tlsClientError + * 2. newSession + * 3. OCSPRequest + * 4. resumeSession + * 5. secureConnection + * 6. keylog + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "tlsClientError", listener: (err: Error, tlsSocket: TLSSocket) => void): this; + addListener( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + addListener( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + addListener( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + addListener(event: "secureConnection", listener: (tlsSocket: TLSSocket) => void): this; + addListener(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: TLSSocket) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "tlsClientError", err: Error, tlsSocket: TLSSocket): boolean; + emit( + event: "newSession", + sessionId: NonSharedBuffer, + sessionData: NonSharedBuffer, + callback: () => void, + ): boolean; + emit( + event: "OCSPRequest", + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ): boolean; + emit( + event: "resumeSession", + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ): boolean; + emit(event: "secureConnection", tlsSocket: TLSSocket): boolean; + emit(event: "keylog", line: NonSharedBuffer, tlsSocket: TLSSocket): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "tlsClientError", listener: (err: Error, tlsSocket: TLSSocket) => void): this; + on( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + on( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + on( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + on(event: "secureConnection", listener: (tlsSocket: TLSSocket) => void): this; + on(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: TLSSocket) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "tlsClientError", listener: (err: Error, tlsSocket: TLSSocket) => void): this; + once( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + once( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + once( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + once(event: "secureConnection", listener: (tlsSocket: TLSSocket) => void): this; + once(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: TLSSocket) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "tlsClientError", listener: (err: Error, tlsSocket: TLSSocket) => void): this; + prependListener( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + prependListener( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + prependListener( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + prependListener(event: "secureConnection", listener: (tlsSocket: TLSSocket) => void): this; + prependListener(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: TLSSocket) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "tlsClientError", listener: (err: Error, tlsSocket: TLSSocket) => void): this; + prependOnceListener( + event: "newSession", + listener: (sessionId: NonSharedBuffer, sessionData: NonSharedBuffer, callback: () => void) => void, + ): this; + prependOnceListener( + event: "OCSPRequest", + listener: ( + certificate: NonSharedBuffer, + issuer: NonSharedBuffer, + callback: (err: Error | null, resp: Buffer | null) => void, + ) => void, + ): this; + prependOnceListener( + event: "resumeSession", + listener: ( + sessionId: NonSharedBuffer, + callback: (err: Error | null, sessionData: Buffer | null) => void, + ) => void, + ): this; + prependOnceListener(event: "secureConnection", listener: (tlsSocket: TLSSocket) => void): this; + prependOnceListener(event: "keylog", listener: (line: NonSharedBuffer, tlsSocket: TLSSocket) => void): this; + } + /** + * @deprecated since v0.11.3 Use `tls.TLSSocket` instead. + */ + interface SecurePair { + encrypted: TLSSocket; + cleartext: TLSSocket; + } + type SecureVersion = "TLSv1.3" | "TLSv1.2" | "TLSv1.1" | "TLSv1"; + interface SecureContextOptions { + /** + * If set, this will be called when a client opens a connection using the ALPN extension. + * One argument will be passed to the callback: an object containing `servername` and `protocols` fields, + * respectively containing the server name from the SNI extension (if any) and an array of + * ALPN protocol name strings. The callback must return either one of the strings listed in `protocols`, + * which will be returned to the client as the selected ALPN protocol, or `undefined`, + * to reject the connection with a fatal alert. If a string is returned that does not match one of + * the client's ALPN protocols, an error will be thrown. + * This option cannot be used with the `ALPNProtocols` option, and setting both options will throw an error. + */ + ALPNCallback?: ((arg: { servername: string; protocols: string[] }) => string | undefined) | undefined; + /** + * Treat intermediate (non-self-signed) + * certificates in the trust CA certificate list as trusted. + * @since v22.9.0, v20.18.0 + */ + allowPartialTrustChain?: boolean | undefined; + /** + * Optionally override the trusted CA certificates. Default is to trust + * the well-known CAs curated by Mozilla. Mozilla's CAs are completely + * replaced when CAs are explicitly specified using this option. + */ + ca?: string | Buffer | Array | undefined; + /** + * Cert chains in PEM format. One cert chain should be provided per + * private key. Each cert chain should consist of the PEM formatted + * certificate for a provided private key, followed by the PEM + * formatted intermediate certificates (if any), in order, and not + * including the root CA (the root CA must be pre-known to the peer, + * see ca). When providing multiple cert chains, they do not have to + * be in the same order as their private keys in key. If the + * intermediate certificates are not provided, the peer will not be + * able to validate the certificate, and the handshake will fail. + */ + cert?: string | Buffer | Array | undefined; + /** + * Colon-separated list of supported signature algorithms. The list + * can contain digest algorithms (SHA256, MD5 etc.), public key + * algorithms (RSA-PSS, ECDSA etc.), combination of both (e.g + * 'RSA+SHA384') or TLS v1.3 scheme names (e.g. rsa_pss_pss_sha512). + */ + sigalgs?: string | undefined; + /** + * Cipher suite specification, replacing the default. For more + * information, see modifying the default cipher suite. Permitted + * ciphers can be obtained via tls.getCiphers(). Cipher names must be + * uppercased in order for OpenSSL to accept them. + */ + ciphers?: string | undefined; + /** + * Name of an OpenSSL engine which can provide the client certificate. + * @deprecated + */ + clientCertEngine?: string | undefined; + /** + * PEM formatted CRLs (Certificate Revocation Lists). + */ + crl?: string | Buffer | Array | undefined; + /** + * `'auto'` or custom Diffie-Hellman parameters, required for non-ECDHE perfect forward secrecy. + * If omitted or invalid, the parameters are silently discarded and DHE ciphers will not be available. + * ECDHE-based perfect forward secrecy will still be available. + */ + dhparam?: string | Buffer | undefined; + /** + * A string describing a named curve or a colon separated list of curve + * NIDs or names, for example P-521:P-384:P-256, to use for ECDH key + * agreement. Set to auto to select the curve automatically. Use + * crypto.getCurves() to obtain a list of available curve names. On + * recent releases, openssl ecparam -list_curves will also display the + * name and description of each available elliptic curve. Default: + * tls.DEFAULT_ECDH_CURVE. + */ + ecdhCurve?: string | undefined; + /** + * Attempt to use the server's cipher suite preferences instead of the + * client's. When true, causes SSL_OP_CIPHER_SERVER_PREFERENCE to be + * set in secureOptions + */ + honorCipherOrder?: boolean | undefined; + /** + * Private keys in PEM format. PEM allows the option of private keys + * being encrypted. Encrypted keys will be decrypted with + * options.passphrase. Multiple keys using different algorithms can be + * provided either as an array of unencrypted key strings or buffers, + * or an array of objects in the form {pem: [, + * passphrase: ]}. The object form can only occur in an array. + * object.passphrase is optional. Encrypted keys will be decrypted with + * object.passphrase if provided, or options.passphrase if it is not. + */ + key?: string | Buffer | Array | undefined; + /** + * Name of an OpenSSL engine to get private key from. Should be used + * together with privateKeyIdentifier. + * @deprecated + */ + privateKeyEngine?: string | undefined; + /** + * Identifier of a private key managed by an OpenSSL engine. Should be + * used together with privateKeyEngine. Should not be set together with + * key, because both options define a private key in different ways. + * @deprecated + */ + privateKeyIdentifier?: string | undefined; + /** + * Optionally set the maximum TLS version to allow. One + * of `'TLSv1.3'`, `'TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified along with the + * `secureProtocol` option, use one or the other. + * **Default:** `'TLSv1.3'`, unless changed using CLI options. Using + * `--tls-max-v1.2` sets the default to `'TLSv1.2'`. Using `--tls-max-v1.3` sets the default to + * `'TLSv1.3'`. If multiple of the options are provided, the highest maximum is used. + */ + maxVersion?: SecureVersion | undefined; + /** + * Optionally set the minimum TLS version to allow. One + * of `'TLSv1.3'`, `'TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified along with the + * `secureProtocol` option, use one or the other. It is not recommended to use + * less than TLSv1.2, but it may be required for interoperability. + * **Default:** `'TLSv1.2'`, unless changed using CLI options. Using + * `--tls-v1.0` sets the default to `'TLSv1'`. Using `--tls-v1.1` sets the default to + * `'TLSv1.1'`. Using `--tls-min-v1.3` sets the default to + * 'TLSv1.3'. If multiple of the options are provided, the lowest minimum is used. + */ + minVersion?: SecureVersion | undefined; + /** + * Shared passphrase used for a single private key and/or a PFX. + */ + passphrase?: string | undefined; + /** + * PFX or PKCS12 encoded private key and certificate chain. pfx is an + * alternative to providing key and cert individually. PFX is usually + * encrypted, if it is, passphrase will be used to decrypt it. Multiple + * PFX can be provided either as an array of unencrypted PFX buffers, + * or an array of objects in the form {buf: [, + * passphrase: ]}. The object form can only occur in an array. + * object.passphrase is optional. Encrypted PFX will be decrypted with + * object.passphrase if provided, or options.passphrase if it is not. + */ + pfx?: string | Buffer | Array | undefined; + /** + * Optionally affect the OpenSSL protocol behavior, which is not + * usually necessary. This should be used carefully if at all! Value is + * a numeric bitmask of the SSL_OP_* options from OpenSSL Options + */ + secureOptions?: number | undefined; // Value is a numeric bitmask of the `SSL_OP_*` options + /** + * Legacy mechanism to select the TLS protocol version to use, it does + * not support independent control of the minimum and maximum version, + * and does not support limiting the protocol to TLSv1.3. Use + * minVersion and maxVersion instead. The possible values are listed as + * SSL_METHODS, use the function names as strings. For example, use + * 'TLSv1_1_method' to force TLS version 1.1, or 'TLS_method' to allow + * any TLS protocol version up to TLSv1.3. It is not recommended to use + * TLS versions less than 1.2, but it may be required for + * interoperability. Default: none, see minVersion. + */ + secureProtocol?: string | undefined; + /** + * Opaque identifier used by servers to ensure session state is not + * shared between applications. Unused by clients. + */ + sessionIdContext?: string | undefined; + /** + * 48-bytes of cryptographically strong pseudo-random data. + * See Session Resumption for more information. + */ + ticketKeys?: Buffer | undefined; + /** + * The number of seconds after which a TLS session created by the + * server will no longer be resumable. See Session Resumption for more + * information. Default: 300. + */ + sessionTimeout?: number | undefined; + } + interface SecureContext { + context: any; + } + /** + * Verifies the certificate `cert` is issued to `hostname`. + * + * Returns [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) object, populating it with `reason`, `host`, and `cert` on + * failure. On success, returns [undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Undefined_type). + * + * This function is intended to be used in combination with the`checkServerIdentity` option that can be passed to {@link connect} and as + * such operates on a `certificate object`. For other purposes, consider using `x509.checkHost()` instead. + * + * This function can be overwritten by providing an alternative function as the `options.checkServerIdentity` option that is passed to `tls.connect()`. The + * overwriting function can call `tls.checkServerIdentity()` of course, to augment + * the checks done with additional verification. + * + * This function is only called if the certificate passed all other checks, such as + * being issued by trusted CA (`options.ca`). + * + * Earlier versions of Node.js incorrectly accepted certificates for a given`hostname` if a matching `uniformResourceIdentifier` subject alternative name + * was present (see [CVE-2021-44531](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44531)). Applications that wish to accept`uniformResourceIdentifier` subject alternative names can use + * a custom `options.checkServerIdentity` function that implements the desired behavior. + * @since v0.8.4 + * @param hostname The host name or IP address to verify the certificate against. + * @param cert A `certificate object` representing the peer's certificate. + */ + function checkServerIdentity(hostname: string, cert: PeerCertificate): Error | undefined; + /** + * Creates a new {@link Server}. The `secureConnectionListener`, if provided, is + * automatically set as a listener for the `'secureConnection'` event. + * + * The `ticketKeys` options is automatically shared between `node:cluster` module + * workers. + * + * The following illustrates a simple echo server: + * + * ```js + * import tls from 'node:tls'; + * import fs from 'node:fs'; + * + * const options = { + * key: fs.readFileSync('server-key.pem'), + * cert: fs.readFileSync('server-cert.pem'), + * + * // This is necessary only if using client certificate authentication. + * requestCert: true, + * + * // This is necessary only if the client uses a self-signed certificate. + * ca: [ fs.readFileSync('client-cert.pem') ], + * }; + * + * const server = tls.createServer(options, (socket) => { + * console.log('server connected', + * socket.authorized ? 'authorized' : 'unauthorized'); + * socket.write('welcome!\n'); + * socket.setEncoding('utf8'); + * socket.pipe(socket); + * }); + * server.listen(8000, () => { + * console.log('server bound'); + * }); + * ``` + * + * The server can be tested by connecting to it using the example client from {@link connect}. + * @since v0.3.2 + */ + function createServer(secureConnectionListener?: (socket: TLSSocket) => void): Server; + function createServer(options: TlsOptions, secureConnectionListener?: (socket: TLSSocket) => void): Server; + /** + * The `callback` function, if specified, will be added as a listener for the `'secureConnect'` event. + * + * `tls.connect()` returns a {@link TLSSocket} object. + * + * Unlike the `https` API, `tls.connect()` does not enable the + * SNI (Server Name Indication) extension by default, which may cause some + * servers to return an incorrect certificate or reject the connection + * altogether. To enable SNI, set the `servername` option in addition + * to `host`. + * + * The following illustrates a client for the echo server example from {@link createServer}: + * + * ```js + * // Assumes an echo server that is listening on port 8000. + * import tls from 'node:tls'; + * import fs from 'node:fs'; + * + * const options = { + * // Necessary only if the server requires client certificate authentication. + * key: fs.readFileSync('client-key.pem'), + * cert: fs.readFileSync('client-cert.pem'), + * + * // Necessary only if the server uses a self-signed certificate. + * ca: [ fs.readFileSync('server-cert.pem') ], + * + * // Necessary only if the server's cert isn't for "localhost". + * checkServerIdentity: () => { return null; }, + * }; + * + * const socket = tls.connect(8000, options, () => { + * console.log('client connected', + * socket.authorized ? 'authorized' : 'unauthorized'); + * process.stdin.pipe(socket); + * process.stdin.resume(); + * }); + * socket.setEncoding('utf8'); + * socket.on('data', (data) => { + * console.log(data); + * }); + * socket.on('end', () => { + * console.log('server ends connection'); + * }); + * ``` + * @since v0.11.3 + */ + function connect(options: ConnectionOptions, secureConnectListener?: () => void): TLSSocket; + function connect( + port: number, + host?: string, + options?: ConnectionOptions, + secureConnectListener?: () => void, + ): TLSSocket; + function connect(port: number, options?: ConnectionOptions, secureConnectListener?: () => void): TLSSocket; + /** + * Creates a new secure pair object with two streams, one of which reads and writes + * the encrypted data and the other of which reads and writes the cleartext data. + * Generally, the encrypted stream is piped to/from an incoming encrypted data + * stream and the cleartext one is used as a replacement for the initial encrypted + * stream. + * + * `tls.createSecurePair()` returns a `tls.SecurePair` object with `cleartext` and `encrypted` stream properties. + * + * Using `cleartext` has the same API as {@link TLSSocket}. + * + * The `tls.createSecurePair()` method is now deprecated in favor of`tls.TLSSocket()`. For example, the code: + * + * ```js + * pair = tls.createSecurePair(// ... ); + * pair.encrypted.pipe(socket); + * socket.pipe(pair.encrypted); + * ``` + * + * can be replaced by: + * + * ```js + * secureSocket = tls.TLSSocket(socket, options); + * ``` + * + * where `secureSocket` has the same API as `pair.cleartext`. + * @since v0.3.2 + * @deprecated Since v0.11.3 - Use {@link TLSSocket} instead. + * @param context A secure context object as returned by `tls.createSecureContext()` + * @param isServer `true` to specify that this TLS connection should be opened as a server. + * @param requestCert `true` to specify whether a server should request a certificate from a connecting client. Only applies when `isServer` is `true`. + * @param rejectUnauthorized If not `false` a server automatically reject clients with invalid certificates. Only applies when `isServer` is `true`. + */ + function createSecurePair( + context?: SecureContext, + isServer?: boolean, + requestCert?: boolean, + rejectUnauthorized?: boolean, + ): SecurePair; + /** + * `{@link createServer}` sets the default value of the `honorCipherOrder` option + * to `true`, other APIs that create secure contexts leave it unset. + * + * `{@link createServer}` uses a 128 bit truncated SHA1 hash value generated + * from `process.argv` as the default value of the `sessionIdContext` option, other + * APIs that create secure contexts have no default value. + * + * The `tls.createSecureContext()` method creates a `SecureContext` object. It is + * usable as an argument to several `tls` APIs, such as `server.addContext()`, + * but has no public methods. The {@link Server} constructor and the {@link createServer} method do not support the `secureContext` option. + * + * A key is _required_ for ciphers that use certificates. Either `key` or `pfx` can be used to provide it. + * + * If the `ca` option is not given, then Node.js will default to using [Mozilla's publicly trusted list of + * CAs](https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt). + * + * Custom DHE parameters are discouraged in favor of the new `dhparam: 'auto' `option. When set to `'auto'`, well-known DHE parameters of sufficient strength + * will be selected automatically. Otherwise, if necessary, `openssl dhparam` can + * be used to create custom parameters. The key length must be greater than or + * equal to 1024 bits or else an error will be thrown. Although 1024 bits is + * permissible, use 2048 bits or larger for stronger security. + * @since v0.11.13 + */ + function createSecureContext(options?: SecureContextOptions): SecureContext; + /** + * Returns an array containing the CA certificates from various sources, depending on `type`: + * + * * `"default"`: return the CA certificates that will be used by the Node.js TLS clients by default. + * * When `--use-bundled-ca` is enabled (default), or `--use-openssl-ca` is not enabled, + * this would include CA certificates from the bundled Mozilla CA store. + * * When `--use-system-ca` is enabled, this would also include certificates from the system's + * trusted store. + * * When `NODE_EXTRA_CA_CERTS` is used, this would also include certificates loaded from the specified + * file. + * * `"system"`: return the CA certificates that are loaded from the system's trusted store, according + * to rules set by `--use-system-ca`. This can be used to get the certificates from the system + * when `--use-system-ca` is not enabled. + * * `"bundled"`: return the CA certificates from the bundled Mozilla CA store. This would be the same + * as `tls.rootCertificates`. + * * `"extra"`: return the CA certificates loaded from `NODE_EXTRA_CA_CERTS`. It's an empty array if + * `NODE_EXTRA_CA_CERTS` is not set. + * @since v22.15.0 + * @param type The type of CA certificates that will be returned. Valid values + * are `"default"`, `"system"`, `"bundled"` and `"extra"`. + * **Default:** `"default"`. + * @returns An array of PEM-encoded certificates. The array may contain duplicates + * if the same certificate is repeatedly stored in multiple sources. + */ + function getCACertificates(type?: "default" | "system" | "bundled" | "extra"): string[]; + /** + * Returns an array with the names of the supported TLS ciphers. The names are + * lower-case for historical reasons, but must be uppercased to be used in + * the `ciphers` option of `{@link createSecureContext}`. + * + * Not all supported ciphers are enabled by default. See + * [Modifying the default TLS cipher suite](https://nodejs.org/docs/latest-v22.x/api/tls.html#modifying-the-default-tls-cipher-suite). + * + * Cipher names that start with `'tls_'` are for TLSv1.3, all the others are for + * TLSv1.2 and below. + * + * ```js + * console.log(tls.getCiphers()); // ['aes128-gcm-sha256', 'aes128-sha', ...] + * ``` + * @since v0.10.2 + */ + function getCiphers(): string[]; + /** + * Sets the default CA certificates used by Node.js TLS clients. If the provided + * certificates are parsed successfully, they will become the default CA + * certificate list returned by {@link getCACertificates} and used + * by subsequent TLS connections that don't specify their own CA certificates. + * The certificates will be deduplicated before being set as the default. + * + * This function only affects the current Node.js thread. Previous + * sessions cached by the HTTPS agent won't be affected by this change, so + * this method should be called before any unwanted cachable TLS connections are + * made. + * + * To use system CA certificates as the default: + * + * ```js + * import tls from 'node:tls'; + * tls.setDefaultCACertificates(tls.getCACertificates('system')); + * ``` + * + * This function completely replaces the default CA certificate list. To add additional + * certificates to the existing defaults, get the current certificates and append to them: + * + * ```js + * import tls from 'node:tls'; + * const currentCerts = tls.getCACertificates('default'); + * const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; + * tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); + * ``` + * @since v22.19.0 + * @param certs An array of CA certificates in PEM format. + */ + function setDefaultCACertificates(certs: ReadonlyArray): void; + /** + * The default curve name to use for ECDH key agreement in a tls server. + * The default value is `'auto'`. See `{@link createSecureContext()}` for further + * information. + * @since v0.11.13 + */ + let DEFAULT_ECDH_CURVE: string; + /** + * The default value of the `maxVersion` option of `{@link createSecureContext()}`. + * It can be assigned any of the supported TLS protocol versions, + * `'TLSv1.3'`, `'TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. **Default:** `'TLSv1.3'`, unless + * changed using CLI options. Using `--tls-max-v1.2` sets the default to `'TLSv1.2'`. Using + * `--tls-max-v1.3` sets the default to `'TLSv1.3'`. If multiple of the options + * are provided, the highest maximum is used. + * @since v11.4.0 + */ + let DEFAULT_MAX_VERSION: SecureVersion; + /** + * The default value of the `minVersion` option of `{@link createSecureContext()}`. + * It can be assigned any of the supported TLS protocol versions, + * `'TLSv1.3'`, `'TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. **Default:** `'TLSv1.2'`, unless + * changed using CLI options. Using `--tls-min-v1.0` sets the default to + * `'TLSv1'`. Using `--tls-min-v1.1` sets the default to `'TLSv1.1'`. Using + * `--tls-min-v1.3` sets the default to `'TLSv1.3'`. If multiple of the options + * are provided, the lowest minimum is used. + * @since v11.4.0 + */ + let DEFAULT_MIN_VERSION: SecureVersion; + /** + * The default value of the `ciphers` option of `{@link createSecureContext()}`. + * It can be assigned any of the supported OpenSSL ciphers. + * Defaults to the content of `crypto.constants.defaultCoreCipherList`, unless + * changed using CLI options using `--tls-default-ciphers`. + * @since v19.8.0 + */ + let DEFAULT_CIPHERS: string; + /** + * An immutable array of strings representing the root certificates (in PEM format) + * from the bundled Mozilla CA store as supplied by the current Node.js version. + * + * The bundled CA store, as supplied by Node.js, is a snapshot of Mozilla CA store + * that is fixed at release time. It is identical on all supported platforms. + * @since v12.3.0 + */ + const rootCertificates: readonly string[]; +} +declare module "node:tls" { + export * from "tls"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/trace_events.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/trace_events.d.ts new file mode 100644 index 00000000..f334b0bc --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/trace_events.d.ts @@ -0,0 +1,197 @@ +/** + * The `node:trace_events` module provides a mechanism to centralize tracing information + * generated by V8, Node.js core, and userspace code. + * + * Tracing can be enabled with the `--trace-event-categories` command-line flag + * or by using the `trace_events` module. The `--trace-event-categories` flag + * accepts a list of comma-separated category names. + * + * The available categories are: + * + * * `node`: An empty placeholder. + * * `node.async_hooks`: Enables capture of detailed [`async_hooks`](https://nodejs.org/docs/latest-v22.x/api/async_hooks.html) trace data. + * The [`async_hooks`](https://nodejs.org/docs/latest-v22.x/api/async_hooks.html) events have a unique `asyncId` and a special `triggerId` `triggerAsyncId` property. + * * `node.bootstrap`: Enables capture of Node.js bootstrap milestones. + * * `node.console`: Enables capture of `console.time()` and `console.count()` output. + * * `node.threadpoolwork.sync`: Enables capture of trace data for threadpool synchronous operations, such as `blob`, `zlib`, `crypto` and `node_api`. + * * `node.threadpoolwork.async`: Enables capture of trace data for threadpool asynchronous operations, such as `blob`, `zlib`, `crypto` and `node_api`. + * * `node.dns.native`: Enables capture of trace data for DNS queries. + * * `node.net.native`: Enables capture of trace data for network. + * * `node.environment`: Enables capture of Node.js Environment milestones. + * * `node.fs.sync`: Enables capture of trace data for file system sync methods. + * * `node.fs_dir.sync`: Enables capture of trace data for file system sync directory methods. + * * `node.fs.async`: Enables capture of trace data for file system async methods. + * * `node.fs_dir.async`: Enables capture of trace data for file system async directory methods. + * * `node.perf`: Enables capture of [Performance API](https://nodejs.org/docs/latest-v22.x/api/perf_hooks.html) measurements. + * * `node.perf.usertiming`: Enables capture of only Performance API User Timing + * measures and marks. + * * `node.perf.timerify`: Enables capture of only Performance API timerify + * measurements. + * * `node.promises.rejections`: Enables capture of trace data tracking the number + * of unhandled Promise rejections and handled-after-rejections. + * * `node.vm.script`: Enables capture of trace data for the `node:vm` module's `runInNewContext()`, `runInContext()`, and `runInThisContext()` methods. + * * `v8`: The [V8](https://nodejs.org/docs/latest-v22.x/api/v8.html) events are GC, compiling, and execution related. + * * `node.http`: Enables capture of trace data for http request / response. + * + * By default the `node`, `node.async_hooks`, and `v8` categories are enabled. + * + * ```bash + * node --trace-event-categories v8,node,node.async_hooks server.js + * ``` + * + * Prior versions of Node.js required the use of the `--trace-events-enabled` flag to enable trace events. This requirement has been removed. However, the `--trace-events-enabled` flag _may_ still be + * used and will enable the `node`, `node.async_hooks`, and `v8` trace event categories by default. + * + * ```bash + * node --trace-events-enabled + * + * # is equivalent to + * + * node --trace-event-categories v8,node,node.async_hooks + * ``` + * + * Alternatively, trace events may be enabled using the `node:trace_events` module: + * + * ```js + * import trace_events from 'node:trace_events'; + * const tracing = trace_events.createTracing({ categories: ['node.perf'] }); + * tracing.enable(); // Enable trace event capture for the 'node.perf' category + * + * // do work + * + * tracing.disable(); // Disable trace event capture for the 'node.perf' category + * ``` + * + * Running Node.js with tracing enabled will produce log files that can be opened + * in the [`chrome://tracing`](https://www.chromium.org/developers/how-tos/trace-event-profiling-tool) tab of Chrome. + * + * The logging file is by default called `node_trace.${rotation}.log`, where `${rotation}` is an incrementing log-rotation id. The filepath pattern can + * be specified with `--trace-event-file-pattern` that accepts a template + * string that supports `${rotation}` and `${pid}`: + * + * ```bash + * node --trace-event-categories v8 --trace-event-file-pattern '${pid}-${rotation}.log' server.js + * ``` + * + * To guarantee that the log file is properly generated after signal events like `SIGINT`, `SIGTERM`, or `SIGBREAK`, make sure to have the appropriate handlers + * in your code, such as: + * + * ```js + * process.on('SIGINT', function onSigint() { + * console.info('Received SIGINT.'); + * process.exit(130); // Or applicable exit code depending on OS and signal + * }); + * ``` + * + * The tracing system uses the same time source + * as the one used by `process.hrtime()`. + * However the trace-event timestamps are expressed in microseconds, + * unlike `process.hrtime()` which returns nanoseconds. + * + * The features from this module are not available in [`Worker`](https://nodejs.org/docs/latest-v22.x/api/worker_threads.html#class-worker) threads. + * @experimental + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/trace_events.js) + */ +declare module "trace_events" { + /** + * The `Tracing` object is used to enable or disable tracing for sets of + * categories. Instances are created using the + * `trace_events.createTracing()` method. + * + * When created, the `Tracing` object is disabled. Calling the + * `tracing.enable()` method adds the categories to the set of enabled trace + * event categories. Calling `tracing.disable()` will remove the categories + * from the set of enabled trace event categories. + */ + interface Tracing { + /** + * A comma-separated list of the trace event categories covered by this + * `Tracing` object. + * @since v10.0.0 + */ + readonly categories: string; + /** + * Disables this `Tracing` object. + * + * Only trace event categories _not_ covered by other enabled `Tracing` + * objects and _not_ specified by the `--trace-event-categories` flag + * will be disabled. + * + * ```js + * import trace_events from 'node:trace_events'; + * const t1 = trace_events.createTracing({ categories: ['node', 'v8'] }); + * const t2 = trace_events.createTracing({ categories: ['node.perf', 'node'] }); + * t1.enable(); + * t2.enable(); + * + * // Prints 'node,node.perf,v8' + * console.log(trace_events.getEnabledCategories()); + * + * t2.disable(); // Will only disable emission of the 'node.perf' category + * + * // Prints 'node,v8' + * console.log(trace_events.getEnabledCategories()); + * ``` + * @since v10.0.0 + */ + disable(): void; + /** + * Enables this `Tracing` object for the set of categories covered by + * the `Tracing` object. + * @since v10.0.0 + */ + enable(): void; + /** + * `true` only if the `Tracing` object has been enabled. + * @since v10.0.0 + */ + readonly enabled: boolean; + } + interface CreateTracingOptions { + /** + * An array of trace category names. Values included in the array are + * coerced to a string when possible. An error will be thrown if the + * value cannot be coerced. + */ + categories: string[]; + } + /** + * Creates and returns a `Tracing` object for the given set of `categories`. + * + * ```js + * import trace_events from 'node:trace_events'; + * const categories = ['node.perf', 'node.async_hooks']; + * const tracing = trace_events.createTracing({ categories }); + * tracing.enable(); + * // do stuff + * tracing.disable(); + * ``` + * @since v10.0.0 + */ + function createTracing(options: CreateTracingOptions): Tracing; + /** + * Returns a comma-separated list of all currently-enabled trace event + * categories. The current set of enabled trace event categories is determined + * by the _union_ of all currently-enabled `Tracing` objects and any categories + * enabled using the `--trace-event-categories` flag. + * + * Given the file `test.js` below, the command `node --trace-event-categories node.perf test.js` will print `'node.async_hooks,node.perf'` to the console. + * + * ```js + * import trace_events from 'node:trace_events'; + * const t1 = trace_events.createTracing({ categories: ['node.async_hooks'] }); + * const t2 = trace_events.createTracing({ categories: ['node.perf'] }); + * const t3 = trace_events.createTracing({ categories: ['v8'] }); + * + * t1.enable(); + * t2.enable(); + * + * console.log(trace_events.getEnabledCategories()); + * ``` + * @since v10.0.0 + */ + function getEnabledCategories(): string | undefined; +} +declare module "node:trace_events" { + export * from "trace_events"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/buffer.buffer.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/buffer.buffer.d.ts new file mode 100644 index 00000000..a5f67d7c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/buffer.buffer.d.ts @@ -0,0 +1,468 @@ +declare module "buffer" { + global { + interface BufferConstructor { + // see ../buffer.d.ts for implementation shared with all TypeScript versions + + /** + * Allocates a new buffer containing the given {str}. + * + * @param str String to store in buffer. + * @param encoding encoding to use, optional. Default is 'utf8' + * @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. + */ + new(str: string, encoding?: BufferEncoding): Buffer; + /** + * Allocates a new buffer of {size} octets. + * + * @param size count of octets to allocate. + * @deprecated since v10.0.0 - Use `Buffer.alloc()` instead (also see `Buffer.allocUnsafe()`). + */ + new(size: number): Buffer; + /** + * Allocates a new buffer containing the given {array} of octets. + * + * @param array The octets to store. + * @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. + */ + new(array: ArrayLike): Buffer; + /** + * Produces a Buffer backed by the same allocated memory as + * the given {ArrayBuffer}/{SharedArrayBuffer}. + * + * @param arrayBuffer The ArrayBuffer with which to share memory. + * @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. + */ + new(arrayBuffer: ArrayBufferLike): Buffer; + /** + * Allocates a new `Buffer` using an `array` of bytes in the range `0` – `255`. + * Array entries outside that range will be truncated to fit into it. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Creates a new Buffer containing the UTF-8 bytes of the string 'buffer'. + * const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); + * ``` + * + * If `array` is an `Array`-like object (that is, one with a `length` property of + * type `number`), it is treated as if it is an array, unless it is a `Buffer` or + * a `Uint8Array`. This means all other `TypedArray` variants get treated as an + * `Array`. To create a `Buffer` from the bytes backing a `TypedArray`, use + * `Buffer.copyBytesFrom()`. + * + * A `TypeError` will be thrown if `array` is not an `Array` or another type + * appropriate for `Buffer.from()` variants. + * + * `Buffer.from(array)` and `Buffer.from(string)` may also use the internal + * `Buffer` pool like `Buffer.allocUnsafe()` does. + * @since v5.10.0 + */ + from(array: WithImplicitCoercion>): Buffer; + /** + * This creates a view of the `ArrayBuffer` without copying the underlying + * memory. For example, when passed a reference to the `.buffer` property of a + * `TypedArray` instance, the newly created `Buffer` will share the same + * allocated memory as the `TypedArray`'s underlying `ArrayBuffer`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const arr = new Uint16Array(2); + * + * arr[0] = 5000; + * arr[1] = 4000; + * + * // Shares memory with `arr`. + * const buf = Buffer.from(arr.buffer); + * + * console.log(buf); + * // Prints: + * + * // Changing the original Uint16Array changes the Buffer also. + * arr[1] = 6000; + * + * console.log(buf); + * // Prints: + * ``` + * + * The optional `byteOffset` and `length` arguments specify a memory range within + * the `arrayBuffer` that will be shared by the `Buffer`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const ab = new ArrayBuffer(10); + * const buf = Buffer.from(ab, 0, 2); + * + * console.log(buf.length); + * // Prints: 2 + * ``` + * + * A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer` or a + * `SharedArrayBuffer` or another type appropriate for `Buffer.from()` + * variants. + * + * It is important to remember that a backing `ArrayBuffer` can cover a range + * of memory that extends beyond the bounds of a `TypedArray` view. A new + * `Buffer` created using the `buffer` property of a `TypedArray` may extend + * beyond the range of the `TypedArray`: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const arrA = Uint8Array.from([0x63, 0x64, 0x65, 0x66]); // 4 elements + * const arrB = new Uint8Array(arrA.buffer, 1, 2); // 2 elements + * console.log(arrA.buffer === arrB.buffer); // true + * + * const buf = Buffer.from(arrB.buffer); + * console.log(buf); + * // Prints: + * ``` + * @since v5.10.0 + * @param arrayBuffer An `ArrayBuffer`, `SharedArrayBuffer`, for example the + * `.buffer` property of a `TypedArray`. + * @param byteOffset Index of first byte to expose. **Default:** `0`. + * @param length Number of bytes to expose. **Default:** + * `arrayBuffer.byteLength - byteOffset`. + */ + from( + arrayBuffer: WithImplicitCoercion, + byteOffset?: number, + length?: number, + ): Buffer; + /** + * Creates a new `Buffer` containing `string`. The `encoding` parameter identifies + * the character encoding to be used when converting `string` into bytes. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('this is a tést'); + * const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex'); + * + * console.log(buf1.toString()); + * // Prints: this is a tést + * console.log(buf2.toString()); + * // Prints: this is a tést + * console.log(buf1.toString('latin1')); + * // Prints: this is a tést + * ``` + * + * A `TypeError` will be thrown if `string` is not a string or another type + * appropriate for `Buffer.from()` variants. + * + * `Buffer.from(string)` may also use the internal `Buffer` pool like + * `Buffer.allocUnsafe()` does. + * @since v5.10.0 + * @param string A string to encode. + * @param encoding The encoding of `string`. **Default:** `'utf8'`. + */ + from(string: WithImplicitCoercion, encoding?: BufferEncoding): Buffer; + from(arrayOrString: WithImplicitCoercion | string>): Buffer; + /** + * Creates a new Buffer using the passed {data} + * @param values to create a new Buffer + */ + of(...items: number[]): Buffer; + /** + * Returns a new `Buffer` which is the result of concatenating all the `Buffer` instances in the `list` together. + * + * If the list has no items, or if the `totalLength` is 0, then a new zero-length `Buffer` is returned. + * + * If `totalLength` is not provided, it is calculated from the `Buffer` instances + * in `list` by adding their lengths. + * + * If `totalLength` is provided, it is coerced to an unsigned integer. If the + * combined length of the `Buffer`s in `list` exceeds `totalLength`, the result is + * truncated to `totalLength`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Create a single `Buffer` from a list of three `Buffer` instances. + * + * const buf1 = Buffer.alloc(10); + * const buf2 = Buffer.alloc(14); + * const buf3 = Buffer.alloc(18); + * const totalLength = buf1.length + buf2.length + buf3.length; + * + * console.log(totalLength); + * // Prints: 42 + * + * const bufA = Buffer.concat([buf1, buf2, buf3], totalLength); + * + * console.log(bufA); + * // Prints: + * console.log(bufA.length); + * // Prints: 42 + * ``` + * + * `Buffer.concat()` may also use the internal `Buffer` pool like `Buffer.allocUnsafe()` does. + * @since v0.7.11 + * @param list List of `Buffer` or {@link Uint8Array} instances to concatenate. + * @param totalLength Total length of the `Buffer` instances in `list` when concatenated. + */ + concat(list: readonly Uint8Array[], totalLength?: number): Buffer; + /** + * Copies the underlying memory of `view` into a new `Buffer`. + * + * ```js + * const u16 = new Uint16Array([0, 0xffff]); + * const buf = Buffer.copyBytesFrom(u16, 1, 1); + * u16[1] = 0; + * console.log(buf.length); // 2 + * console.log(buf[0]); // 255 + * console.log(buf[1]); // 255 + * ``` + * @since v19.8.0 + * @param view The {TypedArray} to copy. + * @param [offset=0] The starting offset within `view`. + * @param [length=view.length - offset] The number of elements from `view` to copy. + */ + copyBytesFrom(view: NodeJS.TypedArray, offset?: number, length?: number): Buffer; + /** + * Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the`Buffer` will be zero-filled. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(5); + * + * console.log(buf); + * // Prints: + * ``` + * + * If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. + * + * If `fill` is specified, the allocated `Buffer` will be initialized by calling `buf.fill(fill)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(5, 'a'); + * + * console.log(buf); + * // Prints: + * ``` + * + * If both `fill` and `encoding` are specified, the allocated `Buffer` will be + * initialized by calling `buf.fill(fill, encoding)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64'); + * + * console.log(buf); + * // Prints: + * ``` + * + * Calling `Buffer.alloc()` can be measurably slower than the alternative `Buffer.allocUnsafe()` but ensures that the newly created `Buffer` instance + * contents will never contain sensitive data from previous allocations, including + * data that might not have been allocated for `Buffer`s. + * + * A `TypeError` will be thrown if `size` is not a number. + * @since v5.10.0 + * @param size The desired length of the new `Buffer`. + * @param [fill=0] A value to pre-fill the new `Buffer` with. + * @param [encoding='utf8'] If `fill` is a string, this is its encoding. + */ + alloc(size: number, fill?: string | Uint8Array | number, encoding?: BufferEncoding): Buffer; + /** + * Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. + * + * The underlying memory for `Buffer` instances created in this way is _not_ + * _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `Buffer.alloc()` instead to initialize`Buffer` instances with zeroes. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(10); + * + * console.log(buf); + * // Prints (contents may vary): + * + * buf.fill(0); + * + * console.log(buf); + * // Prints: + * ``` + * + * A `TypeError` will be thrown if `size` is not a number. + * + * The `Buffer` module pre-allocates an internal `Buffer` instance of + * size `Buffer.poolSize` that is used as a pool for the fast allocation of new `Buffer` instances created using `Buffer.allocUnsafe()`, `Buffer.from(array)`, + * and `Buffer.concat()` only when `size` is less than `Buffer.poolSize >>> 1` (floor of `Buffer.poolSize` divided by two). + * + * Use of this pre-allocated internal memory pool is a key difference between + * calling `Buffer.alloc(size, fill)` vs. `Buffer.allocUnsafe(size).fill(fill)`. + * Specifically, `Buffer.alloc(size, fill)` will _never_ use the internal `Buffer`pool, while `Buffer.allocUnsafe(size).fill(fill)`_will_ use the internal`Buffer` pool if `size` is less + * than or equal to half `Buffer.poolSize`. The + * difference is subtle but can be important when an application requires the + * additional performance that `Buffer.allocUnsafe()` provides. + * @since v5.10.0 + * @param size The desired length of the new `Buffer`. + */ + allocUnsafe(size: number): Buffer; + /** + * Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. A zero-length `Buffer` is created if + * `size` is 0. + * + * The underlying memory for `Buffer` instances created in this way is _not_ + * _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `buf.fill(0)` to initialize + * such `Buffer` instances with zeroes. + * + * When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances, + * allocations under 4 KiB are sliced from a single pre-allocated `Buffer`. This + * allows applications to avoid the garbage collection overhead of creating many + * individually allocated `Buffer` instances. This approach improves both + * performance and memory usage by eliminating the need to track and clean up as + * many individual `ArrayBuffer` objects. + * + * However, in the case where a developer may need to retain a small chunk of + * memory from a pool for an indeterminate amount of time, it may be appropriate + * to create an un-pooled `Buffer` instance using `Buffer.allocUnsafeSlow()` and + * then copying out the relevant bits. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Need to keep around a few small chunks of memory. + * const store = []; + * + * socket.on('readable', () => { + * let data; + * while (null !== (data = readable.read())) { + * // Allocate for retained data. + * const sb = Buffer.allocUnsafeSlow(10); + * + * // Copy the data into the new allocation. + * data.copy(sb, 0, 0, 10); + * + * store.push(sb); + * } + * }); + * ``` + * + * A `TypeError` will be thrown if `size` is not a number. + * @since v5.12.0 + * @param size The desired length of the new `Buffer`. + */ + allocUnsafeSlow(size: number): Buffer; + } + interface Buffer extends Uint8Array { + // see ../buffer.d.ts for implementation shared with all TypeScript versions + + /** + * Returns a new `Buffer` that references the same memory as the original, but + * offset and cropped by the `start` and `end` indices. + * + * This method is not compatible with the `Uint8Array.prototype.slice()`, + * which is a superclass of `Buffer`. To copy the slice, use`Uint8Array.prototype.slice()`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('buffer'); + * + * const copiedBuf = Uint8Array.prototype.slice.call(buf); + * copiedBuf[0]++; + * console.log(copiedBuf.toString()); + * // Prints: cuffer + * + * console.log(buf.toString()); + * // Prints: buffer + * + * // With buf.slice(), the original buffer is modified. + * const notReallyCopiedBuf = buf.slice(); + * notReallyCopiedBuf[0]++; + * console.log(notReallyCopiedBuf.toString()); + * // Prints: cuffer + * console.log(buf.toString()); + * // Also prints: cuffer (!) + * ``` + * @since v0.3.0 + * @deprecated Use `subarray` instead. + * @param [start=0] Where the new `Buffer` will start. + * @param [end=buf.length] Where the new `Buffer` will end (not inclusive). + */ + slice(start?: number, end?: number): Buffer; + /** + * Returns a new `Buffer` that references the same memory as the original, but + * offset and cropped by the `start` and `end` indices. + * + * Specifying `end` greater than `buf.length` will return the same result as + * that of `end` equal to `buf.length`. + * + * This method is inherited from [`TypedArray.prototype.subarray()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray). + * + * Modifying the new `Buffer` slice will modify the memory in the original `Buffer`because the allocated memory of the two objects overlap. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Create a `Buffer` with the ASCII alphabet, take a slice, and modify one byte + * // from the original `Buffer`. + * + * const buf1 = Buffer.allocUnsafe(26); + * + * for (let i = 0; i < 26; i++) { + * // 97 is the decimal ASCII value for 'a'. + * buf1[i] = i + 97; + * } + * + * const buf2 = buf1.subarray(0, 3); + * + * console.log(buf2.toString('ascii', 0, buf2.length)); + * // Prints: abc + * + * buf1[0] = 33; + * + * console.log(buf2.toString('ascii', 0, buf2.length)); + * // Prints: !bc + * ``` + * + * Specifying negative indexes causes the slice to be generated relative to the + * end of `buf` rather than the beginning. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from('buffer'); + * + * console.log(buf.subarray(-6, -1).toString()); + * // Prints: buffe + * // (Equivalent to buf.subarray(0, 5).) + * + * console.log(buf.subarray(-6, -2).toString()); + * // Prints: buff + * // (Equivalent to buf.subarray(0, 4).) + * + * console.log(buf.subarray(-5, -2).toString()); + * // Prints: uff + * // (Equivalent to buf.subarray(1, 4).) + * ``` + * @since v3.0.0 + * @param [start=0] Where the new `Buffer` will start. + * @param [end=buf.length] Where the new `Buffer` will end (not inclusive). + */ + subarray(start?: number, end?: number): Buffer; + } + /** + * @deprecated This is intended for internal use, and will be removed once `@types/node` no longer supports + * TypeScript versions earlier than 5.7. + */ + type NonSharedBuffer = Buffer; + /** + * @deprecated This is intended for internal use, and will be removed once `@types/node` no longer supports + * TypeScript versions earlier than 5.7. + */ + type AllowSharedBuffer = Buffer; + } + /** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */ + var SlowBuffer: { + /** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */ + new(size: number): Buffer; + prototype: Buffer; + }; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/globals.typedarray.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/globals.typedarray.d.ts new file mode 100644 index 00000000..f1c444d1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/globals.typedarray.d.ts @@ -0,0 +1,34 @@ +export {}; // Make this a module + +declare global { + namespace NodeJS { + type TypedArray = + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array + | Int8Array + | Int16Array + | Int32Array + | BigUint64Array + | BigInt64Array + | Float32Array + | Float64Array; + type ArrayBufferView = TypedArray | DataView; + + type NonSharedUint8Array = Uint8Array; + type NonSharedUint8ClampedArray = Uint8ClampedArray; + type NonSharedUint16Array = Uint16Array; + type NonSharedUint32Array = Uint32Array; + type NonSharedInt8Array = Int8Array; + type NonSharedInt16Array = Int16Array; + type NonSharedInt32Array = Int32Array; + type NonSharedBigUint64Array = BigUint64Array; + type NonSharedBigInt64Array = BigInt64Array; + type NonSharedFloat32Array = Float32Array; + type NonSharedFloat64Array = Float64Array; + type NonSharedDataView = DataView; + type NonSharedTypedArray = TypedArray; + type NonSharedArrayBufferView = ArrayBufferView; + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/index.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/index.d.ts new file mode 100644 index 00000000..5a5af42a --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/ts5.6/index.d.ts @@ -0,0 +1,97 @@ +/** + * License for programmatically and manually incorporated + * documentation aka. `JSDoc` from https://github.com/nodejs/node/tree/master/doc + * + * Copyright Node.js contributors. All rights reserved. + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +// NOTE: These definitions support Node.js and TypeScript 4.9 through 5.6. + +// Reference required TypeScript libs: +/// + +// TypeScript backwards-compatibility definitions: +/// + +// Definitions specific to TypeScript 4.9 through 5.6: +/// +/// + +// Definitions for Node.js modules that are not specific to any version of TypeScript: +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/tty.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/tty.d.ts new file mode 100644 index 00000000..f5679466 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/tty.d.ts @@ -0,0 +1,208 @@ +/** + * The `node:tty` module provides the `tty.ReadStream` and `tty.WriteStream` classes. In most cases, it will not be necessary or possible to use this module + * directly. However, it can be accessed using: + * + * ```js + * import tty from 'node:tty'; + * ``` + * + * When Node.js detects that it is being run with a text terminal ("TTY") + * attached, `process.stdin` will, by default, be initialized as an instance of `tty.ReadStream` and both `process.stdout` and `process.stderr` will, by + * default, be instances of `tty.WriteStream`. The preferred method of determining + * whether Node.js is being run within a TTY context is to check that the value of + * the `process.stdout.isTTY` property is `true`: + * + * ```console + * $ node -p -e "Boolean(process.stdout.isTTY)" + * true + * $ node -p -e "Boolean(process.stdout.isTTY)" | cat + * false + * ``` + * + * In most cases, there should be little to no reason for an application to + * manually create instances of the `tty.ReadStream` and `tty.WriteStream` classes. + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/tty.js) + */ +declare module "tty" { + import * as net from "node:net"; + /** + * The `tty.isatty()` method returns `true` if the given `fd` is associated with + * a TTY and `false` if it is not, including whenever `fd` is not a non-negative + * integer. + * @since v0.5.8 + * @param fd A numeric file descriptor + */ + function isatty(fd: number): boolean; + /** + * Represents the readable side of a TTY. In normal circumstances `process.stdin` will be the only `tty.ReadStream` instance in a Node.js + * process and there should be no reason to create additional instances. + * @since v0.5.8 + */ + class ReadStream extends net.Socket { + constructor(fd: number, options?: net.SocketConstructorOpts); + /** + * A `boolean` that is `true` if the TTY is currently configured to operate as a + * raw device. + * + * This flag is always `false` when a process starts, even if the terminal is + * operating in raw mode. Its value will change with subsequent calls to `setRawMode`. + * @since v0.7.7 + */ + isRaw: boolean; + /** + * Allows configuration of `tty.ReadStream` so that it operates as a raw device. + * + * When in raw mode, input is always available character-by-character, not + * including modifiers. Additionally, all special processing of characters by the + * terminal is disabled, including echoing input + * characters. Ctrl+C will no longer cause a `SIGINT` when + * in this mode. + * @since v0.7.7 + * @param mode If `true`, configures the `tty.ReadStream` to operate as a raw device. If `false`, configures the `tty.ReadStream` to operate in its default mode. The `readStream.isRaw` + * property will be set to the resulting mode. + * @return The read stream instance. + */ + setRawMode(mode: boolean): this; + /** + * A `boolean` that is always `true` for `tty.ReadStream` instances. + * @since v0.5.8 + */ + isTTY: boolean; + } + /** + * -1 - to the left from cursor + * 0 - the entire line + * 1 - to the right from cursor + */ + type Direction = -1 | 0 | 1; + /** + * Represents the writable side of a TTY. In normal circumstances, `process.stdout` and `process.stderr` will be the only`tty.WriteStream` instances created for a Node.js process and there + * should be no reason to create additional instances. + * @since v0.5.8 + */ + class WriteStream extends net.Socket { + constructor(fd: number); + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: "resize", listener: () => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: "resize"): boolean; + on(event: string, listener: (...args: any[]) => void): this; + on(event: "resize", listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: "resize", listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "resize", listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "resize", listener: () => void): this; + /** + * `writeStream.clearLine()` clears the current line of this `WriteStream` in a + * direction identified by `dir`. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if the stream wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + clearLine(dir: Direction, callback?: () => void): boolean; + /** + * `writeStream.clearScreenDown()` clears this `WriteStream` from the current + * cursor down. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if the stream wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + clearScreenDown(callback?: () => void): boolean; + /** + * `writeStream.cursorTo()` moves this `WriteStream`'s cursor to the specified + * position. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if the stream wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + cursorTo(x: number, y?: number, callback?: () => void): boolean; + cursorTo(x: number, callback: () => void): boolean; + /** + * `writeStream.moveCursor()` moves this `WriteStream`'s cursor _relative_ to its + * current position. + * @since v0.7.7 + * @param callback Invoked once the operation completes. + * @return `false` if the stream wishes for the calling code to wait for the `'drain'` event to be emitted before continuing to write additional data; otherwise `true`. + */ + moveCursor(dx: number, dy: number, callback?: () => void): boolean; + /** + * Returns: + * + * * `1` for 2, + * * `4` for 16, + * * `8` for 256, + * * `24` for 16,777,216 colors supported. + * + * Use this to determine what colors the terminal supports. Due to the nature of + * colors in terminals it is possible to either have false positives or false + * negatives. It depends on process information and the environment variables that + * may lie about what terminal is used. + * It is possible to pass in an `env` object to simulate the usage of a specific + * terminal. This can be useful to check how specific environment settings behave. + * + * To enforce a specific color support, use one of the below environment settings. + * + * * 2 colors: `FORCE_COLOR = 0` (Disables colors) + * * 16 colors: `FORCE_COLOR = 1` + * * 256 colors: `FORCE_COLOR = 2` + * * 16,777,216 colors: `FORCE_COLOR = 3` + * + * Disabling color support is also possible by using the `NO_COLOR` and `NODE_DISABLE_COLORS` environment variables. + * @since v9.9.0 + * @param [env=process.env] An object containing the environment variables to check. This enables simulating the usage of a specific terminal. + */ + getColorDepth(env?: object): number; + /** + * Returns `true` if the `writeStream` supports at least as many colors as provided + * in `count`. Minimum support is 2 (black and white). + * + * This has the same false positives and negatives as described in `writeStream.getColorDepth()`. + * + * ```js + * process.stdout.hasColors(); + * // Returns true or false depending on if `stdout` supports at least 16 colors. + * process.stdout.hasColors(256); + * // Returns true or false depending on if `stdout` supports at least 256 colors. + * process.stdout.hasColors({ TMUX: '1' }); + * // Returns true. + * process.stdout.hasColors(2 ** 24, { TMUX: '1' }); + * // Returns false (the environment setting pretends to support 2 ** 8 colors). + * ``` + * @since v11.13.0, v10.16.0 + * @param [count=16] The number of colors that are requested (minimum 2). + * @param [env=process.env] An object containing the environment variables to check. This enables simulating the usage of a specific terminal. + */ + hasColors(count?: number): boolean; + hasColors(env?: object): boolean; + hasColors(count: number, env?: object): boolean; + /** + * `writeStream.getWindowSize()` returns the size of the TTY + * corresponding to this `WriteStream`. The array is of the type `[numColumns, numRows]` where `numColumns` and `numRows` represent the number + * of columns and rows in the corresponding TTY. + * @since v0.7.7 + */ + getWindowSize(): [number, number]; + /** + * A `number` specifying the number of columns the TTY currently has. This property + * is updated whenever the `'resize'` event is emitted. + * @since v0.7.7 + */ + columns: number; + /** + * A `number` specifying the number of rows the TTY currently has. This property + * is updated whenever the `'resize'` event is emitted. + * @since v0.7.7 + */ + rows: number; + /** + * A `boolean` that is always `true`. + * @since v0.5.8 + */ + isTTY: boolean; + } +} +declare module "node:tty" { + export * from "tty"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/url.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/url.d.ts new file mode 100644 index 00000000..6a0effc7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/url.d.ts @@ -0,0 +1,984 @@ +/** + * The `node:url` module provides utilities for URL resolution and parsing. It can + * be accessed using: + * + * ```js + * import url from 'node:url'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/url.js) + */ +declare module "url" { + import { Blob as NodeBlob, NonSharedBuffer } from "node:buffer"; + import { ClientRequestArgs } from "node:http"; + import { ParsedUrlQuery, ParsedUrlQueryInput } from "node:querystring"; + // Input to `url.format` + interface UrlObject { + auth?: string | null | undefined; + hash?: string | null | undefined; + host?: string | null | undefined; + hostname?: string | null | undefined; + href?: string | null | undefined; + pathname?: string | null | undefined; + protocol?: string | null | undefined; + search?: string | null | undefined; + slashes?: boolean | null | undefined; + port?: string | number | null | undefined; + query?: string | null | ParsedUrlQueryInput | undefined; + } + // Output of `url.parse` + interface Url { + auth: string | null; + hash: string | null; + host: string | null; + hostname: string | null; + href: string; + path: string | null; + pathname: string | null; + protocol: string | null; + search: string | null; + slashes: boolean | null; + port: string | null; + query: string | null | ParsedUrlQuery; + } + interface UrlWithParsedQuery extends Url { + query: ParsedUrlQuery; + } + interface UrlWithStringQuery extends Url { + query: string | null; + } + interface FileUrlToPathOptions { + /** + * `true` if the `path` should be return as a windows filepath, `false` for posix, and `undefined` for the system default. + * @default undefined + * @since v22.1.0 + */ + windows?: boolean | undefined; + } + interface PathToFileUrlOptions { + /** + * `true` if the `path` should be return as a windows filepath, `false` for posix, and `undefined` for the system default. + * @default undefined + * @since v22.1.0 + */ + windows?: boolean | undefined; + } + /** + * The `url.parse()` method takes a URL string, parses it, and returns a URL + * object. + * + * A `TypeError` is thrown if `urlString` is not a string. + * + * A `URIError` is thrown if the `auth` property is present but cannot be decoded. + * + * `url.parse()` uses a lenient, non-standard algorithm for parsing URL + * strings. It is prone to security issues such as [host name spoofing](https://hackerone.com/reports/678487) and incorrect handling of usernames and passwords. Do not use with untrusted + * input. CVEs are not issued for `url.parse()` vulnerabilities. Use the `WHATWG URL` API instead. + * @since v0.1.25 + * @deprecated Use the WHATWG URL API instead. + * @param urlString The URL string to parse. + * @param [parseQueryString=false] If `true`, the `query` property will always be set to an object returned by the {@link querystring} module's `parse()` method. If `false`, the `query` property + * on the returned URL object will be an unparsed, undecoded string. + * @param [slashesDenoteHost=false] If `true`, the first token after the literal string `//` and preceding the next `/` will be interpreted as the `host`. For instance, given `//foo/bar`, the + * result would be `{host: 'foo', pathname: '/bar'}` rather than `{pathname: '//foo/bar'}`. + */ + function parse(urlString: string): UrlWithStringQuery; + function parse( + urlString: string, + parseQueryString: false | undefined, + slashesDenoteHost?: boolean, + ): UrlWithStringQuery; + function parse(urlString: string, parseQueryString: true, slashesDenoteHost?: boolean): UrlWithParsedQuery; + function parse(urlString: string, parseQueryString: boolean, slashesDenoteHost?: boolean): Url; + /** + * The `url.format()` method returns a formatted URL string derived from `urlObject`. + * + * ```js + * import url from 'node:url'; + * url.format({ + * protocol: 'https', + * hostname: 'example.com', + * pathname: '/some/path', + * query: { + * page: 1, + * format: 'json', + * }, + * }); + * + * // => 'https://example.com/some/path?page=1&format=json' + * ``` + * + * If `urlObject` is not an object or a string, `url.format()` will throw a `TypeError`. + * + * The formatting process operates as follows: + * + * * A new empty string `result` is created. + * * If `urlObject.protocol` is a string, it is appended as-is to `result`. + * * Otherwise, if `urlObject.protocol` is not `undefined` and is not a string, an `Error` is thrown. + * * For all string values of `urlObject.protocol` that _do not end_ with an ASCII + * colon (`:`) character, the literal string `:` will be appended to `result`. + * * If either of the following conditions is true, then the literal string `//` will be appended to `result`: + * * `urlObject.slashes` property is true; + * * `urlObject.protocol` begins with `http`, `https`, `ftp`, `gopher`, or `file`; + * * If the value of the `urlObject.auth` property is truthy, and either `urlObject.host` or `urlObject.hostname` are not `undefined`, the value of `urlObject.auth` will be coerced into a string + * and appended to `result` followed by the literal string `@`. + * * If the `urlObject.host` property is `undefined` then: + * * If the `urlObject.hostname` is a string, it is appended to `result`. + * * Otherwise, if `urlObject.hostname` is not `undefined` and is not a string, + * an `Error` is thrown. + * * If the `urlObject.port` property value is truthy, and `urlObject.hostname` is not `undefined`: + * * The literal string `:` is appended to `result`, and + * * The value of `urlObject.port` is coerced to a string and appended to `result`. + * * Otherwise, if the `urlObject.host` property value is truthy, the value of `urlObject.host` is coerced to a string and appended to `result`. + * * If the `urlObject.pathname` property is a string that is not an empty string: + * * If the `urlObject.pathname` _does not start_ with an ASCII forward slash + * (`/`), then the literal string `'/'` is appended to `result`. + * * The value of `urlObject.pathname` is appended to `result`. + * * Otherwise, if `urlObject.pathname` is not `undefined` and is not a string, an `Error` is thrown. + * * If the `urlObject.search` property is `undefined` and if the `urlObject.query`property is an `Object`, the literal string `?` is appended to `result` followed by the output of calling the + * `querystring` module's `stringify()` method passing the value of `urlObject.query`. + * * Otherwise, if `urlObject.search` is a string: + * * If the value of `urlObject.search` _does not start_ with the ASCII question + * mark (`?`) character, the literal string `?` is appended to `result`. + * * The value of `urlObject.search` is appended to `result`. + * * Otherwise, if `urlObject.search` is not `undefined` and is not a string, an `Error` is thrown. + * * If the `urlObject.hash` property is a string: + * * If the value of `urlObject.hash` _does not start_ with the ASCII hash (`#`) + * character, the literal string `#` is appended to `result`. + * * The value of `urlObject.hash` is appended to `result`. + * * Otherwise, if the `urlObject.hash` property is not `undefined` and is not a + * string, an `Error` is thrown. + * * `result` is returned. + * @since v0.1.25 + * @legacy Use the WHATWG URL API instead. + * @param urlObject A URL object (as returned by `url.parse()` or constructed otherwise). If a string, it is converted to an object by passing it to `url.parse()`. + */ + function format(urlObject: URL, options?: URLFormatOptions): string; + /** + * The `url.format()` method returns a formatted URL string derived from `urlObject`. + * + * ```js + * import url from 'node:url'; + * url.format({ + * protocol: 'https', + * hostname: 'example.com', + * pathname: '/some/path', + * query: { + * page: 1, + * format: 'json', + * }, + * }); + * + * // => 'https://example.com/some/path?page=1&format=json' + * ``` + * + * If `urlObject` is not an object or a string, `url.format()` will throw a `TypeError`. + * + * The formatting process operates as follows: + * + * * A new empty string `result` is created. + * * If `urlObject.protocol` is a string, it is appended as-is to `result`. + * * Otherwise, if `urlObject.protocol` is not `undefined` and is not a string, an `Error` is thrown. + * * For all string values of `urlObject.protocol` that _do not end_ with an ASCII + * colon (`:`) character, the literal string `:` will be appended to `result`. + * * If either of the following conditions is true, then the literal string `//` will be appended to `result`: + * * `urlObject.slashes` property is true; + * * `urlObject.protocol` begins with `http`, `https`, `ftp`, `gopher`, or `file`; + * * If the value of the `urlObject.auth` property is truthy, and either `urlObject.host` or `urlObject.hostname` are not `undefined`, the value of `urlObject.auth` will be coerced into a string + * and appended to `result` followed by the literal string `@`. + * * If the `urlObject.host` property is `undefined` then: + * * If the `urlObject.hostname` is a string, it is appended to `result`. + * * Otherwise, if `urlObject.hostname` is not `undefined` and is not a string, + * an `Error` is thrown. + * * If the `urlObject.port` property value is truthy, and `urlObject.hostname` is not `undefined`: + * * The literal string `:` is appended to `result`, and + * * The value of `urlObject.port` is coerced to a string and appended to `result`. + * * Otherwise, if the `urlObject.host` property value is truthy, the value of `urlObject.host` is coerced to a string and appended to `result`. + * * If the `urlObject.pathname` property is a string that is not an empty string: + * * If the `urlObject.pathname` _does not start_ with an ASCII forward slash + * (`/`), then the literal string `'/'` is appended to `result`. + * * The value of `urlObject.pathname` is appended to `result`. + * * Otherwise, if `urlObject.pathname` is not `undefined` and is not a string, an `Error` is thrown. + * * If the `urlObject.search` property is `undefined` and if the `urlObject.query`property is an `Object`, the literal string `?` is appended to `result` followed by the output of calling the + * `querystring` module's `stringify()` method passing the value of `urlObject.query`. + * * Otherwise, if `urlObject.search` is a string: + * * If the value of `urlObject.search` _does not start_ with the ASCII question + * mark (`?`) character, the literal string `?` is appended to `result`. + * * The value of `urlObject.search` is appended to `result`. + * * Otherwise, if `urlObject.search` is not `undefined` and is not a string, an `Error` is thrown. + * * If the `urlObject.hash` property is a string: + * * If the value of `urlObject.hash` _does not start_ with the ASCII hash (`#`) + * character, the literal string `#` is appended to `result`. + * * The value of `urlObject.hash` is appended to `result`. + * * Otherwise, if the `urlObject.hash` property is not `undefined` and is not a + * string, an `Error` is thrown. + * * `result` is returned. + * @since v0.1.25 + * @legacy Use the WHATWG URL API instead. + * @param urlObject A URL object (as returned by `url.parse()` or constructed otherwise). If a string, it is converted to an object by passing it to `url.parse()`. + */ + function format(urlObject: UrlObject | string): string; + /** + * The `url.resolve()` method resolves a target URL relative to a base URL in a + * manner similar to that of a web browser resolving an anchor tag. + * + * ```js + * import url from 'node:url'; + * url.resolve('/one/two/three', 'four'); // '/one/two/four' + * url.resolve('http://example.com/', '/one'); // 'http://example.com/one' + * url.resolve('http://example.com/one', '/two'); // 'http://example.com/two' + * ``` + * + * To achieve the same result using the WHATWG URL API: + * + * ```js + * function resolve(from, to) { + * const resolvedUrl = new URL(to, new URL(from, 'resolve://')); + * if (resolvedUrl.protocol === 'resolve:') { + * // `from` is a relative URL. + * const { pathname, search, hash } = resolvedUrl; + * return pathname + search + hash; + * } + * return resolvedUrl.toString(); + * } + * + * resolve('/one/two/three', 'four'); // '/one/two/four' + * resolve('http://example.com/', '/one'); // 'http://example.com/one' + * resolve('http://example.com/one', '/two'); // 'http://example.com/two' + * ``` + * @since v0.1.25 + * @legacy Use the WHATWG URL API instead. + * @param from The base URL to use if `to` is a relative URL. + * @param to The target URL to resolve. + */ + function resolve(from: string, to: string): string; + /** + * Returns the [Punycode](https://tools.ietf.org/html/rfc5891#section-4.4) ASCII serialization of the `domain`. If `domain` is an + * invalid domain, the empty string is returned. + * + * It performs the inverse operation to {@link domainToUnicode}. + * + * ```js + * import url from 'node:url'; + * + * console.log(url.domainToASCII('español.com')); + * // Prints xn--espaol-zwa.com + * console.log(url.domainToASCII('中文.com')); + * // Prints xn--fiq228c.com + * console.log(url.domainToASCII('xn--iñvalid.com')); + * // Prints an empty string + * ``` + * @since v7.4.0, v6.13.0 + */ + function domainToASCII(domain: string): string; + /** + * Returns the Unicode serialization of the `domain`. If `domain` is an invalid + * domain, the empty string is returned. + * + * It performs the inverse operation to {@link domainToASCII}. + * + * ```js + * import url from 'node:url'; + * + * console.log(url.domainToUnicode('xn--espaol-zwa.com')); + * // Prints español.com + * console.log(url.domainToUnicode('xn--fiq228c.com')); + * // Prints 中文.com + * console.log(url.domainToUnicode('xn--iñvalid.com')); + * // Prints an empty string + * ``` + * @since v7.4.0, v6.13.0 + */ + function domainToUnicode(domain: string): string; + /** + * This function ensures the correct decodings of percent-encoded characters as + * well as ensuring a cross-platform valid absolute path string. + * + * ```js + * import { fileURLToPath } from 'node:url'; + * + * const __filename = fileURLToPath(import.meta.url); + * + * new URL('file:///C:/path/').pathname; // Incorrect: /C:/path/ + * fileURLToPath('file:///C:/path/'); // Correct: C:\path\ (Windows) + * + * new URL('file://nas/foo.txt').pathname; // Incorrect: /foo.txt + * fileURLToPath('file://nas/foo.txt'); // Correct: \\nas\foo.txt (Windows) + * + * new URL('file:///你好.txt').pathname; // Incorrect: /%E4%BD%A0%E5%A5%BD.txt + * fileURLToPath('file:///你好.txt'); // Correct: /你好.txt (POSIX) + * + * new URL('file:///hello world').pathname; // Incorrect: /hello%20world + * fileURLToPath('file:///hello world'); // Correct: /hello world (POSIX) + * ``` + * @since v10.12.0 + * @param url The file URL string or URL object to convert to a path. + * @return The fully-resolved platform-specific Node.js file path. + */ + function fileURLToPath(url: string | URL, options?: FileUrlToPathOptions): string; + /** + * Like `url.fileURLToPath(...)` except that instead of returning a string + * representation of the path, a `Buffer` is returned. This conversion is + * helpful when the input URL contains percent-encoded segments that are + * not valid UTF-8 / Unicode sequences. + * @since v22.18.0 + * @param url The file URL string or URL object to convert to a path. + * @returns The fully-resolved platform-specific Node.js file path + * as a `Buffer`. + */ + function fileURLToPathBuffer(url: string | URL, options?: FileUrlToPathOptions): NonSharedBuffer; + /** + * This function ensures that `path` is resolved absolutely, and that the URL + * control characters are correctly encoded when converting into a File URL. + * + * ```js + * import { pathToFileURL } from 'node:url'; + * + * new URL('/foo#1', 'file:'); // Incorrect: file:///foo#1 + * pathToFileURL('/foo#1'); // Correct: file:///foo%231 (POSIX) + * + * new URL('/some/path%.c', 'file:'); // Incorrect: file:///some/path%.c + * pathToFileURL('/some/path%.c'); // Correct: file:///some/path%25.c (POSIX) + * ``` + * @since v10.12.0 + * @param path The path to convert to a File URL. + * @return The file URL object. + */ + function pathToFileURL(path: string, options?: PathToFileUrlOptions): URL; + /** + * This utility function converts a URL object into an ordinary options object as + * expected by the `http.request()` and `https.request()` APIs. + * + * ```js + * import { urlToHttpOptions } from 'node:url'; + * const myURL = new URL('https://a:b@測試?abc#foo'); + * + * console.log(urlToHttpOptions(myURL)); + * /* + * { + * protocol: 'https:', + * hostname: 'xn--g6w251d', + * hash: '#foo', + * search: '?abc', + * pathname: '/', + * path: '/?abc', + * href: 'https://a:b@xn--g6w251d/?abc#foo', + * auth: 'a:b' + * } + * + * ``` + * @since v15.7.0, v14.18.0 + * @param url The `WHATWG URL` object to convert to an options object. + * @return Options object + */ + function urlToHttpOptions(url: URL): ClientRequestArgs; + interface URLFormatOptions { + /** + * `true` if the serialized URL string should include the username and password, `false` otherwise. + * @default true + */ + auth?: boolean | undefined; + /** + * `true` if the serialized URL string should include the fragment, `false` otherwise. + * @default true + */ + fragment?: boolean | undefined; + /** + * `true` if the serialized URL string should include the search query, `false` otherwise. + * @default true + */ + search?: boolean | undefined; + /** + * `true` if Unicode characters appearing in the host component of the URL string should be encoded directly as opposed to + * being Punycode encoded. + * @default false + */ + unicode?: boolean | undefined; + } + /** + * Browser-compatible `URL` class, implemented by following the WHATWG URL + * Standard. [Examples of parsed URLs](https://url.spec.whatwg.org/#example-url-parsing) may be found in the Standard itself. + * The `URL` class is also available on the global object. + * + * In accordance with browser conventions, all properties of `URL` objects + * are implemented as getters and setters on the class prototype, rather than as + * data properties on the object itself. Thus, unlike `legacy urlObject`s, + * using the `delete` keyword on any properties of `URL` objects (e.g. `delete myURL.protocol`, `delete myURL.pathname`, etc) has no effect but will still + * return `true`. + * @since v7.0.0, v6.13.0 + */ + class URL { + /** + * Creates a `'blob:nodedata:...'` URL string that represents the given `Blob` object and can be used to retrieve the `Blob` later. + * + * ```js + * import { + * Blob, + * resolveObjectURL, + * } from 'node:buffer'; + * + * const blob = new Blob(['hello']); + * const id = URL.createObjectURL(blob); + * + * // later... + * + * const otherBlob = resolveObjectURL(id); + * console.log(otherBlob.size); + * ``` + * + * The data stored by the registered `Blob` will be retained in memory until `URL.revokeObjectURL()` is called to remove it. + * + * `Blob` objects are registered within the current thread. If using Worker + * Threads, `Blob` objects registered within one Worker will not be available + * to other workers or the main thread. + * @since v16.7.0 + */ + static createObjectURL(blob: NodeBlob): string; + /** + * Removes the stored `Blob` identified by the given ID. Attempting to revoke a + * ID that isn't registered will silently fail. + * @since v16.7.0 + * @param id A `'blob:nodedata:...` URL string returned by a prior call to `URL.createObjectURL()`. + */ + static revokeObjectURL(id: string): void; + /** + * Checks if an `input` relative to the `base` can be parsed to a `URL`. + * + * ```js + * const isValid = URL.canParse('/foo', 'https://example.org/'); // true + * + * const isNotValid = URL.canParse('/foo'); // false + * ``` + * @since v19.9.0 + * @param input The absolute or relative input URL to parse. If `input` is relative, then `base` is required. If `input` is absolute, the `base` is ignored. If `input` is not a string, it is + * `converted to a string` first. + * @param base The base URL to resolve against if the `input` is not absolute. If `base` is not a string, it is `converted to a string` first. + */ + static canParse(input: string, base?: string): boolean; + /** + * Parses a string as a URL. If `base` is provided, it will be used as the base + * URL for the purpose of resolving non-absolute `input` URLs. Returns `null` + * if the parameters can't be resolved to a valid URL. + * @since v22.1.0 + * @param input The absolute or relative input URL to parse. If `input` + * is relative, then `base` is required. If `input` is absolute, the `base` + * is ignored. If `input` is not a string, it is [converted to a string](https://tc39.es/ecma262/#sec-tostring) first. + * @param base The base URL to resolve against if the `input` is not + * absolute. If `base` is not a string, it is [converted to a string](https://tc39.es/ecma262/#sec-tostring) first. + */ + static parse(input: string, base?: string): URL | null; + constructor(input: string | { toString: () => string }, base?: string | URL); + /** + * Gets and sets the fragment portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org/foo#bar'); + * console.log(myURL.hash); + * // Prints #bar + * + * myURL.hash = 'baz'; + * console.log(myURL.href); + * // Prints https://example.org/foo#baz + * ``` + * + * Invalid URL characters included in the value assigned to the `hash` property + * are `percent-encoded`. The selection of which characters to + * percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + hash: string; + /** + * Gets and sets the host portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org:81/foo'); + * console.log(myURL.host); + * // Prints example.org:81 + * + * myURL.host = 'example.com:82'; + * console.log(myURL.href); + * // Prints https://example.com:82/foo + * ``` + * + * Invalid host values assigned to the `host` property are ignored. + */ + host: string; + /** + * Gets and sets the host name portion of the URL. The key difference between`url.host` and `url.hostname` is that `url.hostname` does _not_ include the + * port. + * + * ```js + * const myURL = new URL('https://example.org:81/foo'); + * console.log(myURL.hostname); + * // Prints example.org + * + * // Setting the hostname does not change the port + * myURL.hostname = 'example.com'; + * console.log(myURL.href); + * // Prints https://example.com:81/foo + * + * // Use myURL.host to change the hostname and port + * myURL.host = 'example.org:82'; + * console.log(myURL.href); + * // Prints https://example.org:82/foo + * ``` + * + * Invalid host name values assigned to the `hostname` property are ignored. + */ + hostname: string; + /** + * Gets and sets the serialized URL. + * + * ```js + * const myURL = new URL('https://example.org/foo'); + * console.log(myURL.href); + * // Prints https://example.org/foo + * + * myURL.href = 'https://example.com/bar'; + * console.log(myURL.href); + * // Prints https://example.com/bar + * ``` + * + * Getting the value of the `href` property is equivalent to calling {@link toString}. + * + * Setting the value of this property to a new value is equivalent to creating a + * new `URL` object using `new URL(value)`. Each of the `URL` object's properties will be modified. + * + * If the value assigned to the `href` property is not a valid URL, a `TypeError` will be thrown. + */ + href: string; + /** + * Gets the read-only serialization of the URL's origin. + * + * ```js + * const myURL = new URL('https://example.org/foo/bar?baz'); + * console.log(myURL.origin); + * // Prints https://example.org + * ``` + * + * ```js + * const idnURL = new URL('https://測試'); + * console.log(idnURL.origin); + * // Prints https://xn--g6w251d + * + * console.log(idnURL.hostname); + * // Prints xn--g6w251d + * ``` + */ + readonly origin: string; + /** + * Gets and sets the password portion of the URL. + * + * ```js + * const myURL = new URL('https://abc:xyz@example.com'); + * console.log(myURL.password); + * // Prints xyz + * + * myURL.password = '123'; + * console.log(myURL.href); + * // Prints https://abc:123@example.com/ + * ``` + * + * Invalid URL characters included in the value assigned to the `password` property + * are `percent-encoded`. The selection of which characters to + * percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + password: string; + /** + * Gets and sets the path portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org/abc/xyz?123'); + * console.log(myURL.pathname); + * // Prints /abc/xyz + * + * myURL.pathname = '/abcdef'; + * console.log(myURL.href); + * // Prints https://example.org/abcdef?123 + * ``` + * + * Invalid URL characters included in the value assigned to the `pathname` property are `percent-encoded`. The selection of which characters + * to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + pathname: string; + /** + * Gets and sets the port portion of the URL. + * + * The port value may be a number or a string containing a number in the range `0` to `65535` (inclusive). Setting the value to the default port of the `URL` objects given `protocol` will + * result in the `port` value becoming + * the empty string (`''`). + * + * The port value can be an empty string in which case the port depends on + * the protocol/scheme: + * + * + * + * Upon assigning a value to the port, the value will first be converted to a + * string using `.toString()`. + * + * If that string is invalid but it begins with a number, the leading number is + * assigned to `port`. + * If the number lies outside the range denoted above, it is ignored. + * + * ```js + * const myURL = new URL('https://example.org:8888'); + * console.log(myURL.port); + * // Prints 8888 + * + * // Default ports are automatically transformed to the empty string + * // (HTTPS protocol's default port is 443) + * myURL.port = '443'; + * console.log(myURL.port); + * // Prints the empty string + * console.log(myURL.href); + * // Prints https://example.org/ + * + * myURL.port = 1234; + * console.log(myURL.port); + * // Prints 1234 + * console.log(myURL.href); + * // Prints https://example.org:1234/ + * + * // Completely invalid port strings are ignored + * myURL.port = 'abcd'; + * console.log(myURL.port); + * // Prints 1234 + * + * // Leading numbers are treated as a port number + * myURL.port = '5678abcd'; + * console.log(myURL.port); + * // Prints 5678 + * + * // Non-integers are truncated + * myURL.port = 1234.5678; + * console.log(myURL.port); + * // Prints 1234 + * + * // Out-of-range numbers which are not represented in scientific notation + * // will be ignored. + * myURL.port = 1e10; // 10000000000, will be range-checked as described below + * console.log(myURL.port); + * // Prints 1234 + * ``` + * + * Numbers which contain a decimal point, + * such as floating-point numbers or numbers in scientific notation, + * are not an exception to this rule. + * Leading numbers up to the decimal point will be set as the URL's port, + * assuming they are valid: + * + * ```js + * myURL.port = 4.567e21; + * console.log(myURL.port); + * // Prints 4 (because it is the leading number in the string '4.567e21') + * ``` + */ + port: string; + /** + * Gets and sets the protocol portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org'); + * console.log(myURL.protocol); + * // Prints https: + * + * myURL.protocol = 'ftp'; + * console.log(myURL.href); + * // Prints ftp://example.org/ + * ``` + * + * Invalid URL protocol values assigned to the `protocol` property are ignored. + */ + protocol: string; + /** + * Gets and sets the serialized query portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org/abc?123'); + * console.log(myURL.search); + * // Prints ?123 + * + * myURL.search = 'abc=xyz'; + * console.log(myURL.href); + * // Prints https://example.org/abc?abc=xyz + * ``` + * + * Any invalid URL characters appearing in the value assigned the `search` property will be `percent-encoded`. The selection of which + * characters to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + search: string; + /** + * Gets the `URLSearchParams` object representing the query parameters of the + * URL. This property is read-only but the `URLSearchParams` object it provides + * can be used to mutate the URL instance; to replace the entirety of query + * parameters of the URL, use the {@link search} setter. See `URLSearchParams` documentation for details. + * + * Use care when using `.searchParams` to modify the `URL` because, + * per the WHATWG specification, the `URLSearchParams` object uses + * different rules to determine which characters to percent-encode. For + * instance, the `URL` object will not percent encode the ASCII tilde (`~`) + * character, while `URLSearchParams` will always encode it: + * + * ```js + * const myURL = new URL('https://example.org/abc?foo=~bar'); + * + * console.log(myURL.search); // prints ?foo=~bar + * + * // Modify the URL via searchParams... + * myURL.searchParams.sort(); + * + * console.log(myURL.search); // prints ?foo=%7Ebar + * ``` + */ + readonly searchParams: URLSearchParams; + /** + * Gets and sets the username portion of the URL. + * + * ```js + * const myURL = new URL('https://abc:xyz@example.com'); + * console.log(myURL.username); + * // Prints abc + * + * myURL.username = '123'; + * console.log(myURL.href); + * // Prints https://123:xyz@example.com/ + * ``` + * + * Any invalid URL characters appearing in the value assigned the `username` property will be `percent-encoded`. The selection of which + * characters to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + username: string; + /** + * The `toString()` method on the `URL` object returns the serialized URL. The + * value returned is equivalent to that of {@link href} and {@link toJSON}. + */ + toString(): string; + /** + * The `toJSON()` method on the `URL` object returns the serialized URL. The + * value returned is equivalent to that of {@link href} and {@link toString}. + * + * This method is automatically called when an `URL` object is serialized + * with [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). + * + * ```js + * const myURLs = [ + * new URL('https://www.example.com'), + * new URL('https://test.example.org'), + * ]; + * console.log(JSON.stringify(myURLs)); + * // Prints ["https://www.example.com/","https://test.example.org/"] + * ``` + */ + toJSON(): string; + } + interface URLSearchParamsIterator extends NodeJS.Iterator { + [Symbol.iterator](): URLSearchParamsIterator; + } + /** + * The `URLSearchParams` API provides read and write access to the query of a `URL`. The `URLSearchParams` class can also be used standalone with one of the + * four following constructors. + * The `URLSearchParams` class is also available on the global object. + * + * The WHATWG `URLSearchParams` interface and the `querystring` module have + * similar purpose, but the purpose of the `querystring` module is more + * general, as it allows the customization of delimiter characters (`&` and `=`). + * On the other hand, this API is designed purely for URL query strings. + * + * ```js + * const myURL = new URL('https://example.org/?abc=123'); + * console.log(myURL.searchParams.get('abc')); + * // Prints 123 + * + * myURL.searchParams.append('abc', 'xyz'); + * console.log(myURL.href); + * // Prints https://example.org/?abc=123&abc=xyz + * + * myURL.searchParams.delete('abc'); + * myURL.searchParams.set('a', 'b'); + * console.log(myURL.href); + * // Prints https://example.org/?a=b + * + * const newSearchParams = new URLSearchParams(myURL.searchParams); + * // The above is equivalent to + * // const newSearchParams = new URLSearchParams(myURL.search); + * + * newSearchParams.append('a', 'c'); + * console.log(myURL.href); + * // Prints https://example.org/?a=b + * console.log(newSearchParams.toString()); + * // Prints a=b&a=c + * + * // newSearchParams.toString() is implicitly called + * myURL.search = newSearchParams; + * console.log(myURL.href); + * // Prints https://example.org/?a=b&a=c + * newSearchParams.delete('a'); + * console.log(myURL.href); + * // Prints https://example.org/?a=b&a=c + * ``` + * @since v7.5.0, v6.13.0 + */ + class URLSearchParams implements Iterable<[string, string]> { + constructor( + init?: + | URLSearchParams + | string + | Record + | Iterable<[string, string]> + | ReadonlyArray<[string, string]>, + ); + /** + * Append a new name-value pair to the query string. + */ + append(name: string, value: string): void; + /** + * If `value` is provided, removes all name-value pairs + * where name is `name` and value is `value`. + * + * If `value` is not provided, removes all name-value pairs whose name is `name`. + */ + delete(name: string, value?: string): void; + /** + * Returns an ES6 `Iterator` over each of the name-value pairs in the query. + * Each item of the iterator is a JavaScript `Array`. The first item of the `Array` is the `name`, the second item of the `Array` is the `value`. + * + * Alias for `urlSearchParams[Symbol.iterator]()`. + */ + entries(): URLSearchParamsIterator<[string, string]>; + /** + * Iterates over each name-value pair in the query and invokes the given function. + * + * ```js + * const myURL = new URL('https://example.org/?a=b&c=d'); + * myURL.searchParams.forEach((value, name, searchParams) => { + * console.log(name, value, myURL.searchParams === searchParams); + * }); + * // Prints: + * // a b true + * // c d true + * ``` + * @param fn Invoked for each name-value pair in the query + * @param thisArg To be used as `this` value for when `fn` is called + */ + forEach( + fn: (this: TThis, value: string, name: string, searchParams: URLSearchParams) => void, + thisArg?: TThis, + ): void; + /** + * Returns the value of the first name-value pair whose name is `name`. If there + * are no such pairs, `null` is returned. + * @return or `null` if there is no name-value pair with the given `name`. + */ + get(name: string): string | null; + /** + * Returns the values of all name-value pairs whose name is `name`. If there are + * no such pairs, an empty array is returned. + */ + getAll(name: string): string[]; + /** + * Checks if the `URLSearchParams` object contains key-value pair(s) based on `name` and an optional `value` argument. + * + * If `value` is provided, returns `true` when name-value pair with + * same `name` and `value` exists. + * + * If `value` is not provided, returns `true` if there is at least one name-value + * pair whose name is `name`. + */ + has(name: string, value?: string): boolean; + /** + * Returns an ES6 `Iterator` over the names of each name-value pair. + * + * ```js + * const params = new URLSearchParams('foo=bar&foo=baz'); + * for (const name of params.keys()) { + * console.log(name); + * } + * // Prints: + * // foo + * // foo + * ``` + */ + keys(): URLSearchParamsIterator; + /** + * Sets the value in the `URLSearchParams` object associated with `name` to `value`. If there are any pre-existing name-value pairs whose names are `name`, + * set the first such pair's value to `value` and remove all others. If not, + * append the name-value pair to the query string. + * + * ```js + * const params = new URLSearchParams(); + * params.append('foo', 'bar'); + * params.append('foo', 'baz'); + * params.append('abc', 'def'); + * console.log(params.toString()); + * // Prints foo=bar&foo=baz&abc=def + * + * params.set('foo', 'def'); + * params.set('xyz', 'opq'); + * console.log(params.toString()); + * // Prints foo=def&abc=def&xyz=opq + * ``` + */ + set(name: string, value: string): void; + /** + * The total number of parameter entries. + * @since v19.8.0 + */ + readonly size: number; + /** + * Sort all existing name-value pairs in-place by their names. Sorting is done + * with a [stable sorting algorithm](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability), so relative order between name-value pairs + * with the same name is preserved. + * + * This method can be used, in particular, to increase cache hits. + * + * ```js + * const params = new URLSearchParams('query[]=abc&type=search&query[]=123'); + * params.sort(); + * console.log(params.toString()); + * // Prints query%5B%5D=abc&query%5B%5D=123&type=search + * ``` + * @since v7.7.0, v6.13.0 + */ + sort(): void; + /** + * Returns the search parameters serialized as a string, with characters + * percent-encoded where necessary. + */ + toString(): string; + /** + * Returns an ES6 `Iterator` over the values of each name-value pair. + */ + values(): URLSearchParamsIterator; + [Symbol.iterator](): URLSearchParamsIterator<[string, string]>; + } + import { URL as _URL, URLSearchParams as _URLSearchParams } from "url"; + global { + interface URLSearchParams extends _URLSearchParams {} + interface URL extends _URL {} + interface Global { + URL: typeof _URL; + URLSearchParams: typeof _URLSearchParams; + } + /** + * `URL` class is a global reference for `import { URL } from 'url'` + * https://nodejs.org/api/url.html#the-whatwg-url-api + * @since v10.0.0 + */ + var URL: typeof globalThis extends { + onmessage: any; + URL: infer T; + } ? T + : typeof _URL; + /** + * `URLSearchParams` class is a global reference for `import { URLSearchParams } from 'node:url'` + * https://nodejs.org/api/url.html#class-urlsearchparams + * @since v10.0.0 + */ + var URLSearchParams: typeof globalThis extends { + onmessage: any; + URLSearchParams: infer T; + } ? T + : typeof _URLSearchParams; + } +} +declare module "node:url" { + export * from "url"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/util.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/util.d.ts new file mode 100644 index 00000000..a171f651 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/util.d.ts @@ -0,0 +1,2606 @@ +/** + * The `node:util` module supports the needs of Node.js internal APIs. Many of the + * utilities are useful for application and module developers as well. To access + * it: + * + * ```js + * import util from 'node:util'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/util.js) + */ +declare module "util" { + import * as types from "node:util/types"; + export interface InspectOptions { + /** + * If `true`, object's non-enumerable symbols and properties are included in the formatted result. + * `WeakMap` and `WeakSet` entries are also included as well as user defined prototype properties (excluding method properties). + * @default false + */ + showHidden?: boolean | undefined; + /** + * Specifies the number of times to recurse while formatting object. + * This is useful for inspecting large objects. + * To recurse up to the maximum call stack size pass `Infinity` or `null`. + * @default 2 + */ + depth?: number | null | undefined; + /** + * If `true`, the output is styled with ANSI color codes. Colors are customizable. + */ + colors?: boolean | undefined; + /** + * If `false`, `[util.inspect.custom](depth, opts, inspect)` functions are not invoked. + * @default true + */ + customInspect?: boolean | undefined; + /** + * If `true`, `Proxy` inspection includes the target and handler objects. + * @default false + */ + showProxy?: boolean | undefined; + /** + * Specifies the maximum number of `Array`, `TypedArray`, `WeakMap`, and `WeakSet` elements + * to include when formatting. Set to `null` or `Infinity` to show all elements. + * Set to `0` or negative to show no elements. + * @default 100 + */ + maxArrayLength?: number | null | undefined; + /** + * Specifies the maximum number of characters to + * include when formatting. Set to `null` or `Infinity` to show all elements. + * Set to `0` or negative to show no characters. + * @default 10000 + */ + maxStringLength?: number | null | undefined; + /** + * The length at which input values are split across multiple lines. + * Set to `Infinity` to format the input as a single line + * (in combination with `compact` set to `true` or any number >= `1`). + * @default 80 + */ + breakLength?: number | undefined; + /** + * Setting this to `false` causes each object key + * to be displayed on a new line. It will also add new lines to text that is + * longer than `breakLength`. If set to a number, the most `n` inner elements + * are united on a single line as long as all properties fit into + * `breakLength`. Short array elements are also grouped together. Note that no + * text will be reduced below 16 characters, no matter the `breakLength` size. + * For more information, see the example below. + * @default true + */ + compact?: boolean | number | undefined; + /** + * If set to `true` or a function, all properties of an object, and `Set` and `Map` + * entries are sorted in the resulting string. + * If set to `true` the default sort is used. + * If set to a function, it is used as a compare function. + */ + sorted?: boolean | ((a: string, b: string) => number) | undefined; + /** + * If set to `true`, getters are going to be + * inspected as well. If set to `'get'` only getters without setter are going + * to be inspected. If set to `'set'` only getters having a corresponding + * setter are going to be inspected. This might cause side effects depending on + * the getter function. + * @default false + */ + getters?: "get" | "set" | boolean | undefined; + /** + * If set to `true`, an underscore is used to separate every three digits in all bigints and numbers. + * @default false + */ + numericSeparator?: boolean | undefined; + } + export type Style = + | "special" + | "number" + | "bigint" + | "boolean" + | "undefined" + | "null" + | "string" + | "symbol" + | "date" + | "regexp" + | "module"; + export type CustomInspectFunction = (depth: number, options: InspectOptionsStylized) => any; // TODO: , inspect: inspect + export interface InspectOptionsStylized extends InspectOptions { + stylize(text: string, styleType: Style): string; + } + export interface CallSiteObject { + /** + * Returns the name of the function associated with this call site. + */ + functionName: string; + /** + * Returns the name of the resource that contains the script for the + * function for this call site. + */ + scriptName: string; + /** + * Returns the unique id of the script, as in Chrome DevTools protocol + * [`Runtime.ScriptId`](https://chromedevtools.github.io/devtools-protocol/1-3/Runtime/#type-ScriptId). + * @since v22.14.0 + */ + scriptId: string; + /** + * Returns the number, 1-based, of the line for the associate function call. + */ + lineNumber: number; + /** + * Returns the 1-based column offset on the line for the associated function call. + */ + columnNumber: number; + } + export type DiffEntry = [operation: -1 | 0 | 1, value: string]; + /** + * `util.diff()` compares two string or array values and returns an array of difference entries. + * It uses the Myers diff algorithm to compute minimal differences, which is the same algorithm + * used internally by assertion error messages. + * + * If the values are equal, an empty array is returned. + * + * ```js + * const { diff } = require('node:util'); + * + * // Comparing strings + * const actualString = '12345678'; + * const expectedString = '12!!5!7!'; + * console.log(diff(actualString, expectedString)); + * // [ + * // [0, '1'], + * // [0, '2'], + * // [1, '3'], + * // [1, '4'], + * // [-1, '!'], + * // [-1, '!'], + * // [0, '5'], + * // [1, '6'], + * // [-1, '!'], + * // [0, '7'], + * // [1, '8'], + * // [-1, '!'], + * // ] + * // Comparing arrays + * const actualArray = ['1', '2', '3']; + * const expectedArray = ['1', '3', '4']; + * console.log(diff(actualArray, expectedArray)); + * // [ + * // [0, '1'], + * // [1, '2'], + * // [0, '3'], + * // [-1, '4'], + * // ] + * // Equal values return empty array + * console.log(diff('same', 'same')); + * // [] + * ``` + * @since v22.15.0 + * @experimental + * @param actual The first value to compare + * @param expected The second value to compare + * @returns An array of difference entries. Each entry is an array with two elements: + * * Index 0: `number` Operation code: `-1` for delete, `0` for no-op/unchanged, `1` for insert + * * Index 1: `string` The value associated with the operation + */ + export function diff(actual: string | readonly string[], expected: string | readonly string[]): DiffEntry[]; + /** + * The `util.format()` method returns a formatted string using the first argument + * as a `printf`-like format string which can contain zero or more format + * specifiers. Each specifier is replaced with the converted value from the + * corresponding argument. Supported specifiers are: + * + * If a specifier does not have a corresponding argument, it is not replaced: + * + * ```js + * util.format('%s:%s', 'foo'); + * // Returns: 'foo:%s' + * ``` + * + * Values that are not part of the format string are formatted using `util.inspect()` if their type is not `string`. + * + * If there are more arguments passed to the `util.format()` method than the + * number of specifiers, the extra arguments are concatenated to the returned + * string, separated by spaces: + * + * ```js + * util.format('%s:%s', 'foo', 'bar', 'baz'); + * // Returns: 'foo:bar baz' + * ``` + * + * If the first argument does not contain a valid format specifier, `util.format()` returns a string that is the concatenation of all arguments separated by spaces: + * + * ```js + * util.format(1, 2, 3); + * // Returns: '1 2 3' + * ``` + * + * If only one argument is passed to `util.format()`, it is returned as it is + * without any formatting: + * + * ```js + * util.format('%% %s'); + * // Returns: '%% %s' + * ``` + * + * `util.format()` is a synchronous method that is intended as a debugging tool. + * Some input values can have a significant performance overhead that can block the + * event loop. Use this function with care and never in a hot code path. + * @since v0.5.3 + * @param format A `printf`-like format string. + */ + export function format(format?: any, ...param: any[]): string; + /** + * This function is identical to {@link format}, except in that it takes + * an `inspectOptions` argument which specifies options that are passed along to {@link inspect}. + * + * ```js + * util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 }); + * // Returns 'See object { foo: 42 }', where `42` is colored as a number + * // when printed to a terminal. + * ``` + * @since v10.0.0 + */ + export function formatWithOptions(inspectOptions: InspectOptions, format?: any, ...param: any[]): string; + interface GetCallSitesOptions { + /** + * Reconstruct the original location in the stacktrace from the source-map. + * Enabled by default with the flag `--enable-source-maps`. + */ + sourceMap?: boolean | undefined; + } + /** + * Returns an array of call site objects containing the stack of + * the caller function. + * + * ```js + * import { getCallSites } from 'node:util'; + * + * function exampleFunction() { + * const callSites = getCallSites(); + * + * console.log('Call Sites:'); + * callSites.forEach((callSite, index) => { + * console.log(`CallSite ${index + 1}:`); + * console.log(`Function Name: ${callSite.functionName}`); + * console.log(`Script Name: ${callSite.scriptName}`); + * console.log(`Line Number: ${callSite.lineNumber}`); + * console.log(`Column Number: ${callSite.column}`); + * }); + * // CallSite 1: + * // Function Name: exampleFunction + * // Script Name: /home/example.js + * // Line Number: 5 + * // Column Number: 26 + * + * // CallSite 2: + * // Function Name: anotherFunction + * // Script Name: /home/example.js + * // Line Number: 22 + * // Column Number: 3 + * + * // ... + * } + * + * // A function to simulate another stack layer + * function anotherFunction() { + * exampleFunction(); + * } + * + * anotherFunction(); + * ``` + * + * It is possible to reconstruct the original locations by setting the option `sourceMap` to `true`. + * If the source map is not available, the original location will be the same as the current location. + * When the `--enable-source-maps` flag is enabled, for example when using `--experimental-transform-types`, + * `sourceMap` will be true by default. + * + * ```ts + * import { getCallSites } from 'node:util'; + * + * interface Foo { + * foo: string; + * } + * + * const callSites = getCallSites({ sourceMap: true }); + * + * // With sourceMap: + * // Function Name: '' + * // Script Name: example.js + * // Line Number: 7 + * // Column Number: 26 + * + * // Without sourceMap: + * // Function Name: '' + * // Script Name: example.js + * // Line Number: 2 + * // Column Number: 26 + * ``` + * @param frameCount Number of frames to capture as call site objects. + * **Default:** `10`. Allowable range is between 1 and 200. + * @return An array of call site objects + * @since v22.9.0 + */ + export function getCallSites(frameCount?: number, options?: GetCallSitesOptions): CallSiteObject[]; + export function getCallSites(options: GetCallSitesOptions): CallSiteObject[]; + /** + * Returns the string name for a numeric error code that comes from a Node.js API. + * The mapping between error codes and error names is platform-dependent. + * See `Common System Errors` for the names of common errors. + * + * ```js + * fs.access('file/that/does/not/exist', (err) => { + * const name = util.getSystemErrorName(err.errno); + * console.error(name); // ENOENT + * }); + * ``` + * @since v9.7.0 + */ + export function getSystemErrorName(err: number): string; + /** + * Enable or disable printing a stack trace on `SIGINT`. The API is only available on the main thread. + * @since 22.19.0 + */ + export function setTraceSigInt(enable: boolean): void; + /** + * Returns a Map of all system error codes available from the Node.js API. + * The mapping between error codes and error names is platform-dependent. + * See `Common System Errors` for the names of common errors. + * + * ```js + * fs.access('file/that/does/not/exist', (err) => { + * const errorMap = util.getSystemErrorMap(); + * const name = errorMap.get(err.errno); + * console.error(name); // ENOENT + * }); + * ``` + * @since v16.0.0, v14.17.0 + */ + export function getSystemErrorMap(): Map; + /** + * Returns the string message for a numeric error code that comes from a Node.js + * API. + * The mapping between error codes and string messages is platform-dependent. + * + * ```js + * fs.access('file/that/does/not/exist', (err) => { + * const message = util.getSystemErrorMessage(err.errno); + * console.error(message); // no such file or directory + * }); + * ``` + * @since v22.12.0 + */ + export function getSystemErrorMessage(err: number): string; + /** + * The `util.log()` method prints the given `string` to `stdout` with an included + * timestamp. + * + * ```js + * import util from 'node:util'; + * + * util.log('Timestamped message.'); + * ``` + * @since v0.3.0 + * @deprecated Since v6.0.0 - Use a third party module instead. + */ + export function log(string: string): void; + /** + * Returns the `string` after replacing any surrogate code points + * (or equivalently, any unpaired surrogate code units) with the + * Unicode "replacement character" U+FFFD. + * @since v16.8.0, v14.18.0 + */ + export function toUSVString(string: string): string; + /** + * Creates and returns an `AbortController` instance whose `AbortSignal` is marked + * as transferable and can be used with `structuredClone()` or `postMessage()`. + * @since v18.11.0 + * @returns A transferable AbortController + */ + export function transferableAbortController(): AbortController; + /** + * Marks the given `AbortSignal` as transferable so that it can be used with`structuredClone()` and `postMessage()`. + * + * ```js + * const signal = transferableAbortSignal(AbortSignal.timeout(100)); + * const channel = new MessageChannel(); + * channel.port2.postMessage(signal, [signal]); + * ``` + * @since v18.11.0 + * @param signal The AbortSignal + * @returns The same AbortSignal + */ + export function transferableAbortSignal(signal: AbortSignal): AbortSignal; + /** + * Listens to abort event on the provided `signal` and returns a promise that resolves when the `signal` is aborted. + * If `resource` is provided, it weakly references the operation's associated object, + * so if `resource` is garbage collected before the `signal` aborts, + * then returned promise shall remain pending. + * This prevents memory leaks in long-running or non-cancelable operations. + * + * ```js + * import { aborted } from 'node:util'; + * + * // Obtain an object with an abortable signal, like a custom resource or operation. + * const dependent = obtainSomethingAbortable(); + * + * // Pass `dependent` as the resource, indicating the promise should only resolve + * // if `dependent` is still in memory when the signal is aborted. + * aborted(dependent.signal, dependent).then(() => { + * // This code runs when `dependent` is aborted. + * console.log('Dependent resource was aborted.'); + * }); + * + * // Simulate an event that triggers the abort. + * dependent.on('event', () => { + * dependent.abort(); // This will cause the `aborted` promise to resolve. + * }); + * ``` + * @since v19.7.0 + * @param resource Any non-null object tied to the abortable operation and held weakly. + * If `resource` is garbage collected before the `signal` aborts, the promise remains pending, + * allowing Node.js to stop tracking it. + * This helps prevent memory leaks in long-running or non-cancelable operations. + */ + export function aborted(signal: AbortSignal, resource: any): Promise; + /** + * The `util.inspect()` method returns a string representation of `object` that is + * intended for debugging. The output of `util.inspect` may change at any time + * and should not be depended upon programmatically. Additional `options` may be + * passed that alter the result. + * `util.inspect()` will use the constructor's name and/or `Symbol.toStringTag` + * property to make an identifiable tag for an inspected value. + * + * ```js + * class Foo { + * get [Symbol.toStringTag]() { + * return 'bar'; + * } + * } + * + * class Bar {} + * + * const baz = Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } }); + * + * util.inspect(new Foo()); // 'Foo [bar] {}' + * util.inspect(new Bar()); // 'Bar {}' + * util.inspect(baz); // '[foo] {}' + * ``` + * + * Circular references point to their anchor by using a reference index: + * + * ```js + * import { inspect } from 'node:util'; + * + * const obj = {}; + * obj.a = [obj]; + * obj.b = {}; + * obj.b.inner = obj.b; + * obj.b.obj = obj; + * + * console.log(inspect(obj)); + * // { + * // a: [ [Circular *1] ], + * // b: { inner: [Circular *2], obj: [Circular *1] } + * // } + * ``` + * + * The following example inspects all properties of the `util` object: + * + * ```js + * import util from 'node:util'; + * + * console.log(util.inspect(util, { showHidden: true, depth: null })); + * ``` + * + * The following example highlights the effect of the `compact` option: + * + * ```js + * import { inspect } from 'node:util'; + * + * const o = { + * a: [1, 2, [[ + * 'Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit, sed do ' + + * 'eiusmod \ntempor incididunt ut labore et dolore magna aliqua.', + * 'test', + * 'foo']], 4], + * b: new Map([['za', 1], ['zb', 'test']]), + * }; + * console.log(inspect(o, { compact: true, depth: 5, breakLength: 80 })); + * + * // { a: + * // [ 1, + * // 2, + * // [ [ 'Lorem ipsum dolor sit amet,\nconsectetur [...]', // A long line + * // 'test', + * // 'foo' ] ], + * // 4 ], + * // b: Map(2) { 'za' => 1, 'zb' => 'test' } } + * + * // Setting `compact` to false or an integer creates more reader friendly output. + * console.log(inspect(o, { compact: false, depth: 5, breakLength: 80 })); + * + * // { + * // a: [ + * // 1, + * // 2, + * // [ + * // [ + * // 'Lorem ipsum dolor sit amet,\n' + + * // 'consectetur adipiscing elit, sed do eiusmod \n' + + * // 'tempor incididunt ut labore et dolore magna aliqua.', + * // 'test', + * // 'foo' + * // ] + * // ], + * // 4 + * // ], + * // b: Map(2) { + * // 'za' => 1, + * // 'zb' => 'test' + * // } + * // } + * + * // Setting `breakLength` to e.g. 150 will print the "Lorem ipsum" text in a + * // single line. + * ``` + * + * The `showHidden` option allows `WeakMap` and `WeakSet` entries to be + * inspected. If there are more entries than `maxArrayLength`, there is no + * guarantee which entries are displayed. That means retrieving the same + * `WeakSet` entries twice may result in different output. Furthermore, entries + * with no remaining strong references may be garbage collected at any time. + * + * ```js + * import { inspect } from 'node:util'; + * + * const obj = { a: 1 }; + * const obj2 = { b: 2 }; + * const weakSet = new WeakSet([obj, obj2]); + * + * console.log(inspect(weakSet, { showHidden: true })); + * // WeakSet { { a: 1 }, { b: 2 } } + * ``` + * + * The `sorted` option ensures that an object's property insertion order does not + * impact the result of `util.inspect()`. + * + * ```js + * import { inspect } from 'node:util'; + * import assert from 'node:assert'; + * + * const o1 = { + * b: [2, 3, 1], + * a: '`a` comes before `b`', + * c: new Set([2, 3, 1]), + * }; + * console.log(inspect(o1, { sorted: true })); + * // { a: '`a` comes before `b`', b: [ 2, 3, 1 ], c: Set(3) { 1, 2, 3 } } + * console.log(inspect(o1, { sorted: (a, b) => b.localeCompare(a) })); + * // { c: Set(3) { 3, 2, 1 }, b: [ 2, 3, 1 ], a: '`a` comes before `b`' } + * + * const o2 = { + * c: new Set([2, 1, 3]), + * a: '`a` comes before `b`', + * b: [2, 3, 1], + * }; + * assert.strict.equal( + * inspect(o1, { sorted: true }), + * inspect(o2, { sorted: true }), + * ); + * ``` + * + * The `numericSeparator` option adds an underscore every three digits to all + * numbers. + * + * ```js + * import { inspect } from 'node:util'; + * + * const thousand = 1000; + * const million = 1000000; + * const bigNumber = 123456789n; + * const bigDecimal = 1234.12345; + * + * console.log(inspect(thousand, { numericSeparator: true })); + * // 1_000 + * console.log(inspect(million, { numericSeparator: true })); + * // 1_000_000 + * console.log(inspect(bigNumber, { numericSeparator: true })); + * // 123_456_789n + * console.log(inspect(bigDecimal, { numericSeparator: true })); + * // 1_234.123_45 + * ``` + * + * `util.inspect()` is a synchronous method intended for debugging. Its maximum + * output length is approximately 128 MiB. Inputs that result in longer output will + * be truncated. + * @since v0.3.0 + * @param object Any JavaScript primitive or `Object`. + * @return The representation of `object`. + */ + export function inspect(object: any, showHidden?: boolean, depth?: number | null, color?: boolean): string; + export function inspect(object: any, options?: InspectOptions): string; + export namespace inspect { + let colors: NodeJS.Dict<[number, number]>; + let styles: { + [K in Style]: string; + }; + let defaultOptions: InspectOptions; + /** + * Allows changing inspect settings from the repl. + */ + let replDefaults: InspectOptions; + /** + * That can be used to declare custom inspect functions. + */ + const custom: unique symbol; + } + /** + * Alias for [`Array.isArray()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray). + * + * Returns `true` if the given `object` is an `Array`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isArray([]); + * // Returns: true + * util.isArray(new Array()); + * // Returns: true + * util.isArray({}); + * // Returns: false + * ``` + * @since v0.6.0 + * @deprecated Since v4.0.0 - Use `isArray` instead. + */ + export function isArray(object: unknown): object is unknown[]; + /** + * Returns `true` if the given `object` is a `RegExp`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isRegExp(/some regexp/); + * // Returns: true + * util.isRegExp(new RegExp('another regexp')); + * // Returns: true + * util.isRegExp({}); + * // Returns: false + * ``` + * @since v0.6.0 + * @deprecated Since v4.0.0 - Deprecated + */ + export function isRegExp(object: unknown): object is RegExp; + /** + * Returns `true` if the given `object` is a `Date`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isDate(new Date()); + * // Returns: true + * util.isDate(Date()); + * // false (without 'new' returns a String) + * util.isDate({}); + * // Returns: false + * ``` + * @since v0.6.0 + * @deprecated Since v4.0.0 - Use {@link types.isDate} instead. + */ + export function isDate(object: unknown): object is Date; + /** + * Returns `true` if the given `object` is an `Error`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isError(new Error()); + * // Returns: true + * util.isError(new TypeError()); + * // Returns: true + * util.isError({ name: 'Error', message: 'an error occurred' }); + * // Returns: false + * ``` + * + * This method relies on `Object.prototype.toString()` behavior. It is + * possible to obtain an incorrect result when the `object` argument manipulates `@@toStringTag`. + * + * ```js + * import util from 'node:util'; + * const obj = { name: 'Error', message: 'an error occurred' }; + * + * util.isError(obj); + * // Returns: false + * obj[Symbol.toStringTag] = 'Error'; + * util.isError(obj); + * // Returns: true + * ``` + * @since v0.6.0 + * @deprecated Since v4.0.0 - Use {@link types.isNativeError} instead. + */ + export function isError(object: unknown): object is Error; + /** + * Usage of `util.inherits()` is discouraged. Please use the ES6 `class` and + * `extends` keywords to get language level inheritance support. Also note + * that the two styles are [semantically incompatible](https://github.com/nodejs/node/issues/4179). + * + * Inherit the prototype methods from one + * [constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor) into another. The + * prototype of `constructor` will be set to a new object created from + * `superConstructor`. + * + * This mainly adds some input validation on top of + * `Object.setPrototypeOf(constructor.prototype, superConstructor.prototype)`. + * As an additional convenience, `superConstructor` will be accessible + * through the `constructor.super_` property. + * + * ```js + * const util = require('node:util'); + * const EventEmitter = require('node:events'); + * + * function MyStream() { + * EventEmitter.call(this); + * } + * + * util.inherits(MyStream, EventEmitter); + * + * MyStream.prototype.write = function(data) { + * this.emit('data', data); + * }; + * + * const stream = new MyStream(); + * + * console.log(stream instanceof EventEmitter); // true + * console.log(MyStream.super_ === EventEmitter); // true + * + * stream.on('data', (data) => { + * console.log(`Received data: "${data}"`); + * }); + * stream.write('It works!'); // Received data: "It works!" + * ``` + * + * ES6 example using `class` and `extends`: + * + * ```js + * import EventEmitter from 'node:events'; + * + * class MyStream extends EventEmitter { + * write(data) { + * this.emit('data', data); + * } + * } + * + * const stream = new MyStream(); + * + * stream.on('data', (data) => { + * console.log(`Received data: "${data}"`); + * }); + * stream.write('With ES6'); + * ``` + * @since v0.3.0 + * @legacy Use ES2015 class syntax and `extends` keyword instead. + */ + export function inherits(constructor: unknown, superConstructor: unknown): void; + export type DebugLoggerFunction = (msg: string, ...param: unknown[]) => void; + export interface DebugLogger extends DebugLoggerFunction { + /** + * The `util.debuglog().enabled` getter is used to create a test that can be used + * in conditionals based on the existence of the `NODE_DEBUG` environment variable. + * If the `section` name appears within the value of that environment variable, + * then the returned value will be `true`. If not, then the returned value will be + * `false`. + * + * ```js + * import { debuglog } from 'node:util'; + * const enabled = debuglog('foo').enabled; + * if (enabled) { + * console.log('hello from foo [%d]', 123); + * } + * ``` + * + * If this program is run with `NODE_DEBUG=foo` in the environment, then it will + * output something like: + * + * ```console + * hello from foo [123] + * ``` + */ + enabled: boolean; + } + /** + * The `util.debuglog()` method is used to create a function that conditionally + * writes debug messages to `stderr` based on the existence of the `NODE_DEBUG` + * environment variable. If the `section` name appears within the value of that + * environment variable, then the returned function operates similar to + * `console.error()`. If not, then the returned function is a no-op. + * + * ```js + * import { debuglog } from 'node:util'; + * const log = debuglog('foo'); + * + * log('hello from foo [%d]', 123); + * ``` + * + * If this program is run with `NODE_DEBUG=foo` in the environment, then + * it will output something like: + * + * ```console + * FOO 3245: hello from foo [123] + * ``` + * + * where `3245` is the process id. If it is not run with that + * environment variable set, then it will not print anything. + * + * The `section` supports wildcard also: + * + * ```js + * import { debuglog } from 'node:util'; + * const log = debuglog('foo'); + * + * log('hi there, it\'s foo-bar [%d]', 2333); + * ``` + * + * if it is run with `NODE_DEBUG=foo*` in the environment, then it will output + * something like: + * + * ```console + * FOO-BAR 3257: hi there, it's foo-bar [2333] + * ``` + * + * Multiple comma-separated `section` names may be specified in the `NODE_DEBUG` + * environment variable: `NODE_DEBUG=fs,net,tls`. + * + * The optional `callback` argument can be used to replace the logging function + * with a different function that doesn't have any initialization or + * unnecessary wrapping. + * + * ```js + * import { debuglog } from 'node:util'; + * let log = debuglog('internals', (debug) => { + * // Replace with a logging function that optimizes out + * // testing if the section is enabled + * log = debug; + * }); + * ``` + * @since v0.11.3 + * @param section A string identifying the portion of the application for which the `debuglog` function is being created. + * @param callback A callback invoked the first time the logging function is called with a function argument that is a more optimized logging function. + * @return The logging function + */ + export function debuglog(section: string, callback?: (fn: DebugLoggerFunction) => void): DebugLogger; + export { debuglog as debug }; + /** + * Returns `true` if the given `object` is a `Boolean`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isBoolean(1); + * // Returns: false + * util.isBoolean(0); + * // Returns: false + * util.isBoolean(false); + * // Returns: true + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `typeof value === 'boolean'` instead. + */ + export function isBoolean(object: unknown): object is boolean; + /** + * Returns `true` if the given `object` is a `Buffer`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isBuffer({ length: 0 }); + * // Returns: false + * util.isBuffer([]); + * // Returns: false + * util.isBuffer(Buffer.from('hello world')); + * // Returns: true + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `isBuffer` instead. + */ + export function isBuffer(object: unknown): object is Buffer; + /** + * Returns `true` if the given `object` is a `Function`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * function Foo() {} + * const Bar = () => {}; + * + * util.isFunction({}); + * // Returns: false + * util.isFunction(Foo); + * // Returns: true + * util.isFunction(Bar); + * // Returns: true + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `typeof value === 'function'` instead. + */ + export function isFunction(object: unknown): boolean; + /** + * Returns `true` if the given `object` is strictly `null`. Otherwise, returns`false`. + * + * ```js + * import util from 'node:util'; + * + * util.isNull(0); + * // Returns: false + * util.isNull(undefined); + * // Returns: false + * util.isNull(null); + * // Returns: true + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `value === null` instead. + */ + export function isNull(object: unknown): object is null; + /** + * Returns `true` if the given `object` is `null` or `undefined`. Otherwise, + * returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isNullOrUndefined(0); + * // Returns: false + * util.isNullOrUndefined(undefined); + * // Returns: true + * util.isNullOrUndefined(null); + * // Returns: true + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `value === undefined || value === null` instead. + */ + export function isNullOrUndefined(object: unknown): object is null | undefined; + /** + * Returns `true` if the given `object` is a `Number`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isNumber(false); + * // Returns: false + * util.isNumber(Infinity); + * // Returns: true + * util.isNumber(0); + * // Returns: true + * util.isNumber(NaN); + * // Returns: true + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `typeof value === 'number'` instead. + */ + export function isNumber(object: unknown): object is number; + /** + * Returns `true` if the given `object` is strictly an `Object`**and** not a`Function` (even though functions are objects in JavaScript). + * Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isObject(5); + * // Returns: false + * util.isObject(null); + * // Returns: false + * util.isObject({}); + * // Returns: true + * util.isObject(() => {}); + * // Returns: false + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `value !== null && typeof value === 'object'` instead. + */ + export function isObject(object: unknown): boolean; + /** + * Returns `true` if the given `object` is a primitive type. Otherwise, returns`false`. + * + * ```js + * import util from 'node:util'; + * + * util.isPrimitive(5); + * // Returns: true + * util.isPrimitive('foo'); + * // Returns: true + * util.isPrimitive(false); + * // Returns: true + * util.isPrimitive(null); + * // Returns: true + * util.isPrimitive(undefined); + * // Returns: true + * util.isPrimitive({}); + * // Returns: false + * util.isPrimitive(() => {}); + * // Returns: false + * util.isPrimitive(/^$/); + * // Returns: false + * util.isPrimitive(new Date()); + * // Returns: false + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `(typeof value !== 'object' && typeof value !== 'function') || value === null` instead. + */ + export function isPrimitive(object: unknown): boolean; + /** + * Returns `true` if the given `object` is a `string`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isString(''); + * // Returns: true + * util.isString('foo'); + * // Returns: true + * util.isString(String('foo')); + * // Returns: true + * util.isString(5); + * // Returns: false + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `typeof value === 'string'` instead. + */ + export function isString(object: unknown): object is string; + /** + * Returns `true` if the given `object` is a `Symbol`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * util.isSymbol(5); + * // Returns: false + * util.isSymbol('foo'); + * // Returns: false + * util.isSymbol(Symbol('foo')); + * // Returns: true + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `typeof value === 'symbol'` instead. + */ + export function isSymbol(object: unknown): object is symbol; + /** + * Returns `true` if the given `object` is `undefined`. Otherwise, returns `false`. + * + * ```js + * import util from 'node:util'; + * + * const foo = undefined; + * util.isUndefined(5); + * // Returns: false + * util.isUndefined(foo); + * // Returns: true + * util.isUndefined(null); + * // Returns: false + * ``` + * @since v0.11.5 + * @deprecated Since v4.0.0 - Use `value === undefined` instead. + */ + export function isUndefined(object: unknown): object is undefined; + /** + * The `util.deprecate()` method wraps `fn` (which may be a function or class) in + * such a way that it is marked as deprecated. + * + * ```js + * import { deprecate } from 'node:util'; + * + * export const obsoleteFunction = deprecate(() => { + * // Do something here. + * }, 'obsoleteFunction() is deprecated. Use newShinyFunction() instead.'); + * ``` + * + * When called, `util.deprecate()` will return a function that will emit a + * `DeprecationWarning` using the `'warning'` event. The warning will + * be emitted and printed to `stderr` the first time the returned function is + * called. After the warning is emitted, the wrapped function is called without + * emitting a warning. + * + * If the same optional `code` is supplied in multiple calls to `util.deprecate()`, + * the warning will be emitted only once for that `code`. + * + * ```js + * import { deprecate } from 'node:util'; + * + * const fn1 = deprecate( + * () => 'a value', + * 'deprecation message', + * 'DEP0001', + * ); + * const fn2 = deprecate( + * () => 'a different value', + * 'other dep message', + * 'DEP0001', + * ); + * fn1(); // Emits a deprecation warning with code DEP0001 + * fn2(); // Does not emit a deprecation warning because it has the same code + * ``` + * + * If either the `--no-deprecation` or `--no-warnings` command-line flags are + * used, or if the `process.noDeprecation` property is set to `true` _prior_ to + * the first deprecation warning, the `util.deprecate()` method does nothing. + * + * If the `--trace-deprecation` or `--trace-warnings` command-line flags are set, + * or the `process.traceDeprecation` property is set to `true`, a warning and a + * stack trace are printed to `stderr` the first time the deprecated function is + * called. + * + * If the `--throw-deprecation` command-line flag is set, or the + * `process.throwDeprecation` property is set to `true`, then an exception will be + * thrown when the deprecated function is called. + * + * The `--throw-deprecation` command-line flag and `process.throwDeprecation` + * property take precedence over `--trace-deprecation` and + * `process.traceDeprecation`. + * @since v0.8.0 + * @param fn The function that is being deprecated. + * @param msg A warning message to display when the deprecated function is invoked. + * @param code A deprecation code. See the `list of deprecated APIs` for a list of codes. + * @return The deprecated function wrapped to emit a warning. + */ + export function deprecate(fn: T, msg: string, code?: string): T; + /** + * Returns `true` if there is deep strict equality between `val1` and `val2`. + * Otherwise, returns `false`. + * + * See `assert.deepStrictEqual()` for more information about deep strict + * equality. + * @since v9.0.0 + */ + export function isDeepStrictEqual(val1: unknown, val2: unknown): boolean; + /** + * Returns `str` with any ANSI escape codes removed. + * + * ```js + * console.log(util.stripVTControlCharacters('\u001B[4mvalue\u001B[0m')); + * // Prints "value" + * ``` + * @since v16.11.0 + */ + export function stripVTControlCharacters(str: string): string; + /** + * Takes an `async` function (or a function that returns a `Promise`) and returns a + * function following the error-first callback style, i.e. taking + * an `(err, value) => ...` callback as the last argument. In the callback, the + * first argument will be the rejection reason (or `null` if the `Promise` + * resolved), and the second argument will be the resolved value. + * + * ```js + * import { callbackify } from 'node:util'; + * + * async function fn() { + * return 'hello world'; + * } + * const callbackFunction = callbackify(fn); + * + * callbackFunction((err, ret) => { + * if (err) throw err; + * console.log(ret); + * }); + * ``` + * + * Will print: + * + * ```text + * hello world + * ``` + * + * The callback is executed asynchronously, and will have a limited stack trace. + * If the callback throws, the process will emit an `'uncaughtException'` + * event, and if not handled will exit. + * + * Since `null` has a special meaning as the first argument to a callback, if a + * wrapped function rejects a `Promise` with a falsy value as a reason, the value + * is wrapped in an `Error` with the original value stored in a field named + * `reason`. + * + * ```js + * function fn() { + * return Promise.reject(null); + * } + * const callbackFunction = util.callbackify(fn); + * + * callbackFunction((err, ret) => { + * // When the Promise was rejected with `null` it is wrapped with an Error and + * // the original value is stored in `reason`. + * err && Object.hasOwn(err, 'reason') && err.reason === null; // true + * }); + * ``` + * @since v8.2.0 + * @param fn An `async` function + * @return a callback style function + */ + export function callbackify(fn: () => Promise): (callback: (err: NodeJS.ErrnoException) => void) => void; + export function callbackify( + fn: () => Promise, + ): (callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1) => Promise, + ): (arg1: T1, callback: (err: NodeJS.ErrnoException) => void) => void; + export function callbackify( + fn: (arg1: T1) => Promise, + ): (arg1: T1, callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise, + ): (arg1: T1, arg2: T2, callback: (err: NodeJS.ErrnoException) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise, + ): (arg1: T1, arg2: T2, callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, + ): (arg1: T1, arg2: T2, arg3: T3, callback: (err: NodeJS.ErrnoException) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, + ): (arg1: T1, arg2: T2, arg3: T3, callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise, + ): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, callback: (err: NodeJS.ErrnoException) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise, + ): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, + ) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise, + ): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, callback: (err: NodeJS.ErrnoException) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise, + ): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, + ) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6) => Promise, + ): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + arg6: T6, + callback: (err: NodeJS.ErrnoException) => void, + ) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6) => Promise, + ): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + arg6: T6, + callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, + ) => void; + export interface CustomPromisifyLegacy extends Function { + __promisify__: TCustom; + } + export interface CustomPromisifySymbol extends Function { + [promisify.custom]: TCustom; + } + export type CustomPromisify = + | CustomPromisifySymbol + | CustomPromisifyLegacy; + /** + * Takes a function following the common error-first callback style, i.e. taking + * an `(err, value) => ...` callback as the last argument, and returns a version + * that returns promises. + * + * ```js + * import { promisify } from 'node:util'; + * import { stat } from 'node:fs'; + * + * const promisifiedStat = promisify(stat); + * promisifiedStat('.').then((stats) => { + * // Do something with `stats` + * }).catch((error) => { + * // Handle the error. + * }); + * ``` + * + * Or, equivalently using `async function`s: + * + * ```js + * import { promisify } from 'node:util'; + * import { stat } from 'node:fs'; + * + * const promisifiedStat = promisify(stat); + * + * async function callStat() { + * const stats = await promisifiedStat('.'); + * console.log(`This directory is owned by ${stats.uid}`); + * } + * + * callStat(); + * ``` + * + * If there is an `original[util.promisify.custom]` property present, `promisify` + * will return its value, see [Custom promisified functions](https://nodejs.org/docs/latest-v22.x/api/util.html#custom-promisified-functions). + * + * `promisify()` assumes that `original` is a function taking a callback as its + * final argument in all cases. If `original` is not a function, `promisify()` + * will throw an error. If `original` is a function but its last argument is not + * an error-first callback, it will still be passed an error-first + * callback as its last argument. + * + * Using `promisify()` on class methods or other methods that use `this` may not + * work as expected unless handled specially: + * + * ```js + * import { promisify } from 'node:util'; + * + * class Foo { + * constructor() { + * this.a = 42; + * } + * + * bar(callback) { + * callback(null, this.a); + * } + * } + * + * const foo = new Foo(); + * + * const naiveBar = promisify(foo.bar); + * // TypeError: Cannot read properties of undefined (reading 'a') + * // naiveBar().then(a => console.log(a)); + * + * naiveBar.call(foo).then((a) => console.log(a)); // '42' + * + * const bindBar = naiveBar.bind(foo); + * bindBar().then((a) => console.log(a)); // '42' + * ``` + * @since v8.0.0 + */ + export function promisify(fn: CustomPromisify): TCustom; + export function promisify( + fn: (callback: (err: any, result: TResult) => void) => void, + ): () => Promise; + export function promisify(fn: (callback: (err?: any) => void) => void): () => Promise; + export function promisify( + fn: (arg1: T1, callback: (err: any, result: TResult) => void) => void, + ): (arg1: T1) => Promise; + export function promisify(fn: (arg1: T1, callback: (err?: any) => void) => void): (arg1: T1) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, callback: (err: any, result: TResult) => void) => void, + ): (arg1: T1, arg2: T2) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, callback: (err?: any) => void) => void, + ): (arg1: T1, arg2: T2) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, arg3: T3, callback: (err: any, result: TResult) => void) => void, + ): (arg1: T1, arg2: T2, arg3: T3) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, arg3: T3, callback: (err?: any) => void) => void, + ): (arg1: T1, arg2: T2, arg3: T3) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, callback: (err: any, result: TResult) => void) => void, + ): (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, callback: (err?: any) => void) => void, + ): (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, callback: (err: any, result: TResult) => void) => void, + ): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise; + export function promisify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, callback: (err?: any) => void) => void, + ): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise; + export function promisify(fn: Function): Function; + export namespace promisify { + /** + * That can be used to declare custom promisified variants of functions. + */ + const custom: unique symbol; + } + /** + * Stability: 1.1 - Active development + * Given an example `.env` file: + * + * ```js + * import { parseEnv } from 'node:util'; + * + * parseEnv('HELLO=world\nHELLO=oh my\n'); + * // Returns: { HELLO: 'oh my' } + * ``` + * @param content The raw contents of a `.env` file. + * @since v20.12.0 + */ + export function parseEnv(content: string): NodeJS.Dict; + // https://nodejs.org/docs/latest/api/util.html#foreground-colors + type ForegroundColors = + | "black" + | "blackBright" + | "blue" + | "blueBright" + | "cyan" + | "cyanBright" + | "gray" + | "green" + | "greenBright" + | "grey" + | "magenta" + | "magentaBright" + | "red" + | "redBright" + | "white" + | "whiteBright" + | "yellow" + | "yellowBright"; + // https://nodejs.org/docs/latest/api/util.html#background-colors + type BackgroundColors = + | "bgBlack" + | "bgBlackBright" + | "bgBlue" + | "bgBlueBright" + | "bgCyan" + | "bgCyanBright" + | "bgGray" + | "bgGreen" + | "bgGreenBright" + | "bgGrey" + | "bgMagenta" + | "bgMagentaBright" + | "bgRed" + | "bgRedBright" + | "bgWhite" + | "bgWhiteBright" + | "bgYellow" + | "bgYellowBright"; + // https://nodejs.org/docs/latest/api/util.html#modifiers + type Modifiers = + | "blink" + | "bold" + | "dim" + | "doubleunderline" + | "framed" + | "hidden" + | "inverse" + | "italic" + | "none" + | "overlined" + | "reset" + | "strikethrough" + | "underline"; + export interface StyleTextOptions { + /** + * When true, `stream` is checked to see if it can handle colors. + * @default true + */ + validateStream?: boolean | undefined; + /** + * A stream that will be validated if it can be colored. + * @default process.stdout + */ + stream?: NodeJS.WritableStream | undefined; + } + /** + * This function returns a formatted text considering the `format` passed + * for printing in a terminal. It is aware of the terminal's capabilities + * and acts according to the configuration set via `NO_COLOR`, + * `NODE_DISABLE_COLORS` and `FORCE_COLOR` environment variables. + * + * ```js + * import { styleText } from 'node:util'; + * import { stderr } from 'node:process'; + * + * const successMessage = styleText('green', 'Success!'); + * console.log(successMessage); + * + * const errorMessage = styleText( + * 'red', + * 'Error! Error!', + * // Validate if process.stderr has TTY + * { stream: stderr }, + * ); + * console.error(errorMessage); + * ``` + * + * `util.inspect.colors` also provides text formats such as `italic`, and + * `underline` and you can combine both: + * + * ```js + * console.log( + * util.styleText(['underline', 'italic'], 'My italic underlined message'), + * ); + * ``` + * + * When passing an array of formats, the order of the format applied + * is left to right so the following style might overwrite the previous one. + * + * ```js + * console.log( + * util.styleText(['red', 'green'], 'text'), // green + * ); + * ``` + * + * The special format value `none` applies no additional styling to the text. + * + * The full list of formats can be found in [modifiers](https://nodejs.org/docs/latest-v22.x/api/util.html#modifiers). + * @param format A text format or an Array of text formats defined in `util.inspect.colors`. + * @param text The text to to be formatted. + * @since v20.12.0 + */ + export function styleText( + format: + | ForegroundColors + | BackgroundColors + | Modifiers + | Array, + text: string, + options?: StyleTextOptions, + ): string; + /** + * An implementation of the [WHATWG Encoding Standard](https://encoding.spec.whatwg.org/) `TextDecoder` API. + * + * ```js + * const decoder = new TextDecoder(); + * const u8arr = new Uint8Array([72, 101, 108, 108, 111]); + * console.log(decoder.decode(u8arr)); // Hello + * ``` + * @since v8.3.0 + */ + export class TextDecoder { + /** + * The encoding supported by the `TextDecoder` instance. + */ + readonly encoding: string; + /** + * The value will be `true` if decoding errors result in a `TypeError` being + * thrown. + */ + readonly fatal: boolean; + /** + * The value will be `true` if the decoding result will include the byte order + * mark. + */ + readonly ignoreBOM: boolean; + constructor( + encoding?: string, + options?: { + fatal?: boolean | undefined; + ignoreBOM?: boolean | undefined; + }, + ); + /** + * Decodes the `input` and returns a string. If `options.stream` is `true`, any + * incomplete byte sequences occurring at the end of the `input` are buffered + * internally and emitted after the next call to `textDecoder.decode()`. + * + * If `textDecoder.fatal` is `true`, decoding errors that occur will result in a `TypeError` being thrown. + * @param input An `ArrayBuffer`, `DataView`, or `TypedArray` instance containing the encoded data. + */ + decode( + input?: NodeJS.ArrayBufferView | ArrayBuffer | null, + options?: { + stream?: boolean | undefined; + }, + ): string; + } + export interface EncodeIntoResult { + /** + * The read Unicode code units of input. + */ + read: number; + /** + * The written UTF-8 bytes of output. + */ + written: number; + } + export { types }; + + //// TextEncoder/Decoder + /** + * An implementation of the [WHATWG Encoding Standard](https://encoding.spec.whatwg.org/) `TextEncoder` API. All + * instances of `TextEncoder` only support UTF-8 encoding. + * + * ```js + * const encoder = new TextEncoder(); + * const uint8array = encoder.encode('this is some data'); + * ``` + * + * The `TextEncoder` class is also available on the global object. + * @since v8.3.0 + */ + export class TextEncoder { + /** + * The encoding supported by the `TextEncoder` instance. Always set to `'utf-8'`. + */ + readonly encoding: string; + /** + * UTF-8 encodes the `input` string and returns a `Uint8Array` containing the + * encoded bytes. + * @param [input='an empty string'] The text to encode. + */ + encode(input?: string): NodeJS.NonSharedUint8Array; + /** + * UTF-8 encodes the `src` string to the `dest` Uint8Array and returns an object + * containing the read Unicode code units and written UTF-8 bytes. + * + * ```js + * const encoder = new TextEncoder(); + * const src = 'this is some data'; + * const dest = new Uint8Array(10); + * const { read, written } = encoder.encodeInto(src, dest); + * ``` + * @param src The text to encode. + * @param dest The array to hold the encode result. + */ + encodeInto(src: string, dest: Uint8Array): EncodeIntoResult; + } + import { TextDecoder as _TextDecoder, TextEncoder as _TextEncoder } from "util"; + global { + /** + * `TextDecoder` class is a global reference for `import { TextDecoder } from 'node:util'` + * https://nodejs.org/api/globals.html#textdecoder + * @since v11.0.0 + */ + var TextDecoder: typeof globalThis extends { + onmessage: any; + TextDecoder: infer TextDecoder; + } ? TextDecoder + : typeof _TextDecoder; + /** + * `TextEncoder` class is a global reference for `import { TextEncoder } from 'node:util'` + * https://nodejs.org/api/globals.html#textencoder + * @since v11.0.0 + */ + var TextEncoder: typeof globalThis extends { + onmessage: any; + TextEncoder: infer TextEncoder; + } ? TextEncoder + : typeof _TextEncoder; + } + + //// parseArgs + /** + * Provides a higher level API for command-line argument parsing than interacting + * with `process.argv` directly. Takes a specification for the expected arguments + * and returns a structured object with the parsed options and positionals. + * + * ```js + * import { parseArgs } from 'node:util'; + * const args = ['-f', '--bar', 'b']; + * const options = { + * foo: { + * type: 'boolean', + * short: 'f', + * }, + * bar: { + * type: 'string', + * }, + * }; + * const { + * values, + * positionals, + * } = parseArgs({ args, options }); + * console.log(values, positionals); + * // Prints: [Object: null prototype] { foo: true, bar: 'b' } [] + * ``` + * @since v18.3.0, v16.17.0 + * @param config Used to provide arguments for parsing and to configure the parser. `config` supports the following properties: + * @return The parsed command line arguments: + */ + export function parseArgs(config?: T): ParsedResults; + + /** + * Type of argument used in {@link parseArgs}. + */ + export type ParseArgsOptionsType = "boolean" | "string"; + + export interface ParseArgsOptionDescriptor { + /** + * Type of argument. + */ + type: ParseArgsOptionsType; + /** + * Whether this option can be provided multiple times. + * If `true`, all values will be collected in an array. + * If `false`, values for the option are last-wins. + * @default false. + */ + multiple?: boolean | undefined; + /** + * A single character alias for the option. + */ + short?: string | undefined; + /** + * The value to assign to + * the option if it does not appear in the arguments to be parsed. The value + * must match the type specified by the `type` property. If `multiple` is + * `true`, it must be an array. No default value is applied when the option + * does appear in the arguments to be parsed, even if the provided value + * is falsy. + * @since v18.11.0 + */ + default?: string | boolean | string[] | boolean[] | undefined; + } + export interface ParseArgsOptionsConfig { + [longOption: string]: ParseArgsOptionDescriptor; + } + export interface ParseArgsConfig { + /** + * Array of argument strings. + */ + args?: readonly string[] | undefined; + /** + * Used to describe arguments known to the parser. + */ + options?: ParseArgsOptionsConfig | undefined; + /** + * Should an error be thrown when unknown arguments are encountered, + * or when arguments are passed that do not match the `type` configured in `options`. + * @default true + */ + strict?: boolean | undefined; + /** + * Whether this command accepts positional arguments. + */ + allowPositionals?: boolean | undefined; + /** + * If `true`, allows explicitly setting boolean options to `false` by prefixing the option name with `--no-`. + * @default false + * @since v22.4.0 + */ + allowNegative?: boolean | undefined; + /** + * Return the parsed tokens. This is useful for extending the built-in behavior, + * from adding additional checks through to reprocessing the tokens in different ways. + * @default false + */ + tokens?: boolean | undefined; + } + /* + IfDefaultsTrue and IfDefaultsFalse are helpers to handle default values for missing boolean properties. + TypeScript does not have exact types for objects: https://github.com/microsoft/TypeScript/issues/12936 + This means it is impossible to distinguish between "field X is definitely not present" and "field X may or may not be present". + But we expect users to generally provide their config inline or `as const`, which means TS will always know whether a given field is present. + So this helper treats "not definitely present" (i.e., not `extends boolean`) as being "definitely not present", i.e. it should have its default value. + This is technically incorrect but is a much nicer UX for the common case. + The IfDefaultsTrue version is for things which default to true; the IfDefaultsFalse version is for things which default to false. + */ + type IfDefaultsTrue = T extends true ? IfTrue + : T extends false ? IfFalse + : IfTrue; + + // we put the `extends false` condition first here because `undefined` compares like `any` when `strictNullChecks: false` + type IfDefaultsFalse = T extends false ? IfFalse + : T extends true ? IfTrue + : IfFalse; + + type ExtractOptionValue = IfDefaultsTrue< + T["strict"], + O["type"] extends "string" ? string : O["type"] extends "boolean" ? boolean : string | boolean, + string | boolean + >; + + type ApplyOptionalModifiers> = ( + & { -readonly [LongOption in keyof O]?: V[LongOption] } + & { [LongOption in keyof O as O[LongOption]["default"] extends {} ? LongOption : never]: V[LongOption] } + ) extends infer P ? { [K in keyof P]: P[K] } : never; // resolve intersection to object + + type ParsedValues = + & IfDefaultsTrue + & (T["options"] extends ParseArgsOptionsConfig ? ApplyOptionalModifiers< + T["options"], + { + [LongOption in keyof T["options"]]: IfDefaultsFalse< + T["options"][LongOption]["multiple"], + Array>, + ExtractOptionValue + >; + } + > + : {}); + + type ParsedPositionals = IfDefaultsTrue< + T["strict"], + IfDefaultsFalse, + IfDefaultsTrue + >; + + type PreciseTokenForOptions< + K extends string, + O extends ParseArgsOptionDescriptor, + > = O["type"] extends "string" ? { + kind: "option"; + index: number; + name: K; + rawName: string; + value: string; + inlineValue: boolean; + } + : O["type"] extends "boolean" ? { + kind: "option"; + index: number; + name: K; + rawName: string; + value: undefined; + inlineValue: undefined; + } + : OptionToken & { name: K }; + + type TokenForOptions< + T extends ParseArgsConfig, + K extends keyof T["options"] = keyof T["options"], + > = K extends unknown + ? T["options"] extends ParseArgsOptionsConfig ? PreciseTokenForOptions + : OptionToken + : never; + + type ParsedOptionToken = IfDefaultsTrue, OptionToken>; + + type ParsedPositionalToken = IfDefaultsTrue< + T["strict"], + IfDefaultsFalse, + IfDefaultsTrue + >; + + type ParsedTokens = Array< + ParsedOptionToken | ParsedPositionalToken | { kind: "option-terminator"; index: number } + >; + + type PreciseParsedResults = IfDefaultsFalse< + T["tokens"], + { + values: ParsedValues; + positionals: ParsedPositionals; + tokens: ParsedTokens; + }, + { + values: ParsedValues; + positionals: ParsedPositionals; + } + >; + + type OptionToken = + | { kind: "option"; index: number; name: string; rawName: string; value: string; inlineValue: boolean } + | { + kind: "option"; + index: number; + name: string; + rawName: string; + value: undefined; + inlineValue: undefined; + }; + + type Token = + | OptionToken + | { kind: "positional"; index: number; value: string } + | { kind: "option-terminator"; index: number }; + + // If ParseArgsConfig extends T, then the user passed config constructed elsewhere. + // So we can't rely on the `"not definitely present" implies "definitely not present"` assumption mentioned above. + type ParsedResults = ParseArgsConfig extends T ? { + values: { + [longOption: string]: undefined | string | boolean | Array; + }; + positionals: string[]; + tokens?: Token[]; + } + : PreciseParsedResults; + + /** + * An implementation of [the MIMEType class](https://bmeck.github.io/node-proposal-mime-api/). + * + * In accordance with browser conventions, all properties of `MIMEType` objects + * are implemented as getters and setters on the class prototype, rather than as + * data properties on the object itself. + * + * A MIME string is a structured string containing multiple meaningful + * components. When parsed, a `MIMEType` object is returned containing + * properties for each of these components. + * @since v19.1.0, v18.13.0 + */ + export class MIMEType { + /** + * Creates a new MIMEType object by parsing the input. + * + * A `TypeError` will be thrown if the `input` is not a valid MIME. + * Note that an effort will be made to coerce the given values into strings. + * @param input The input MIME to parse. + */ + constructor(input: string | { toString: () => string }); + + /** + * Gets and sets the type portion of the MIME. + * + * ```js + * import { MIMEType } from 'node:util'; + * + * const myMIME = new MIMEType('text/javascript'); + * console.log(myMIME.type); + * // Prints: text + * myMIME.type = 'application'; + * console.log(myMIME.type); + * // Prints: application + * console.log(String(myMIME)); + * // Prints: application/javascript + * ``` + */ + type: string; + /** + * Gets and sets the subtype portion of the MIME. + * + * ```js + * import { MIMEType } from 'node:util'; + * + * const myMIME = new MIMEType('text/ecmascript'); + * console.log(myMIME.subtype); + * // Prints: ecmascript + * myMIME.subtype = 'javascript'; + * console.log(myMIME.subtype); + * // Prints: javascript + * console.log(String(myMIME)); + * // Prints: text/javascript + * ``` + */ + subtype: string; + /** + * Gets the essence of the MIME. This property is read only. + * Use `mime.type` or `mime.subtype` to alter the MIME. + * + * ```js + * import { MIMEType } from 'node:util'; + * + * const myMIME = new MIMEType('text/javascript;key=value'); + * console.log(myMIME.essence); + * // Prints: text/javascript + * myMIME.type = 'application'; + * console.log(myMIME.essence); + * // Prints: application/javascript + * console.log(String(myMIME)); + * // Prints: application/javascript;key=value + * ``` + */ + readonly essence: string; + /** + * Gets the `MIMEParams` object representing the + * parameters of the MIME. This property is read-only. See `MIMEParams` documentation for details. + */ + readonly params: MIMEParams; + /** + * The `toString()` method on the `MIMEType` object returns the serialized MIME. + * + * Because of the need for standard compliance, this method does not allow users + * to customize the serialization process of the MIME. + */ + toString(): string; + } + /** + * The `MIMEParams` API provides read and write access to the parameters of a `MIMEType`. + * @since v19.1.0, v18.13.0 + */ + export class MIMEParams { + /** + * Remove all name-value pairs whose name is `name`. + */ + delete(name: string): void; + /** + * Returns an iterator over each of the name-value pairs in the parameters. + * Each item of the iterator is a JavaScript `Array`. The first item of the array + * is the `name`, the second item of the array is the `value`. + */ + entries(): NodeJS.Iterator<[name: string, value: string]>; + /** + * Returns the value of the first name-value pair whose name is `name`. If there + * are no such pairs, `null` is returned. + * @return or `null` if there is no name-value pair with the given `name`. + */ + get(name: string): string | null; + /** + * Returns `true` if there is at least one name-value pair whose name is `name`. + */ + has(name: string): boolean; + /** + * Returns an iterator over the names of each name-value pair. + * + * ```js + * import { MIMEType } from 'node:util'; + * + * const { params } = new MIMEType('text/plain;foo=0;bar=1'); + * for (const name of params.keys()) { + * console.log(name); + * } + * // Prints: + * // foo + * // bar + * ``` + */ + keys(): NodeJS.Iterator; + /** + * Sets the value in the `MIMEParams` object associated with `name` to `value`. If there are any pre-existing name-value pairs whose names are `name`, + * set the first such pair's value to `value`. + * + * ```js + * import { MIMEType } from 'node:util'; + * + * const { params } = new MIMEType('text/plain;foo=0;bar=1'); + * params.set('foo', 'def'); + * params.set('baz', 'xyz'); + * console.log(params.toString()); + * // Prints: foo=def;bar=1;baz=xyz + * ``` + */ + set(name: string, value: string): void; + /** + * Returns an iterator over the values of each name-value pair. + */ + values(): NodeJS.Iterator; + /** + * Returns an iterator over each of the name-value pairs in the parameters. + */ + [Symbol.iterator](): NodeJS.Iterator<[name: string, value: string]>; + } +} +declare module "util/types" { + import { KeyObject, webcrypto } from "node:crypto"; + /** + * Returns `true` if the value is a built-in [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) or + * [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) instance. + * + * See also `util.types.isArrayBuffer()` and `util.types.isSharedArrayBuffer()`. + * + * ```js + * util.types.isAnyArrayBuffer(new ArrayBuffer()); // Returns true + * util.types.isAnyArrayBuffer(new SharedArrayBuffer()); // Returns true + * ``` + * @since v10.0.0 + */ + function isAnyArrayBuffer(object: unknown): object is ArrayBufferLike; + /** + * Returns `true` if the value is an `arguments` object. + * + * ```js + * function foo() { + * util.types.isArgumentsObject(arguments); // Returns true + * } + * ``` + * @since v10.0.0 + */ + function isArgumentsObject(object: unknown): object is IArguments; + /** + * Returns `true` if the value is a built-in [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) instance. + * This does _not_ include [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) instances. Usually, it is + * desirable to test for both; See `util.types.isAnyArrayBuffer()` for that. + * + * ```js + * util.types.isArrayBuffer(new ArrayBuffer()); // Returns true + * util.types.isArrayBuffer(new SharedArrayBuffer()); // Returns false + * ``` + * @since v10.0.0 + */ + function isArrayBuffer(object: unknown): object is ArrayBuffer; + /** + * Returns `true` if the value is an instance of one of the [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) views, such as typed + * array objects or [`DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView). Equivalent to + * [`ArrayBuffer.isView()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/isView). + * + * ```js + * util.types.isArrayBufferView(new Int8Array()); // true + * util.types.isArrayBufferView(Buffer.from('hello world')); // true + * util.types.isArrayBufferView(new DataView(new ArrayBuffer(16))); // true + * util.types.isArrayBufferView(new ArrayBuffer()); // false + * ``` + * @since v10.0.0 + */ + function isArrayBufferView(object: unknown): object is NodeJS.ArrayBufferView; + /** + * Returns `true` if the value is an [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function). + * This only reports back what the JavaScript engine is seeing; + * in particular, the return value may not match the original source code if + * a transpilation tool was used. + * + * ```js + * util.types.isAsyncFunction(function foo() {}); // Returns false + * util.types.isAsyncFunction(async function foo() {}); // Returns true + * ``` + * @since v10.0.0 + */ + function isAsyncFunction(object: unknown): boolean; + /** + * Returns `true` if the value is a `BigInt64Array` instance. + * + * ```js + * util.types.isBigInt64Array(new BigInt64Array()); // Returns true + * util.types.isBigInt64Array(new BigUint64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isBigInt64Array(value: unknown): value is BigInt64Array; + /** + * Returns `true` if the value is a BigInt object, e.g. created + * by `Object(BigInt(123))`. + * + * ```js + * util.types.isBigIntObject(Object(BigInt(123))); // Returns true + * util.types.isBigIntObject(BigInt(123)); // Returns false + * util.types.isBigIntObject(123); // Returns false + * ``` + * @since v10.4.0 + */ + function isBigIntObject(object: unknown): object is BigInt; + /** + * Returns `true` if the value is a `BigUint64Array` instance. + * + * ```js + * util.types.isBigUint64Array(new BigInt64Array()); // Returns false + * util.types.isBigUint64Array(new BigUint64Array()); // Returns true + * ``` + * @since v10.0.0 + */ + function isBigUint64Array(value: unknown): value is BigUint64Array; + /** + * Returns `true` if the value is a boolean object, e.g. created + * by `new Boolean()`. + * + * ```js + * util.types.isBooleanObject(false); // Returns false + * util.types.isBooleanObject(true); // Returns false + * util.types.isBooleanObject(new Boolean(false)); // Returns true + * util.types.isBooleanObject(new Boolean(true)); // Returns true + * util.types.isBooleanObject(Boolean(false)); // Returns false + * util.types.isBooleanObject(Boolean(true)); // Returns false + * ``` + * @since v10.0.0 + */ + function isBooleanObject(object: unknown): object is Boolean; + /** + * Returns `true` if the value is any boxed primitive object, e.g. created + * by `new Boolean()`, `new String()` or `Object(Symbol())`. + * + * For example: + * + * ```js + * util.types.isBoxedPrimitive(false); // Returns false + * util.types.isBoxedPrimitive(new Boolean(false)); // Returns true + * util.types.isBoxedPrimitive(Symbol('foo')); // Returns false + * util.types.isBoxedPrimitive(Object(Symbol('foo'))); // Returns true + * util.types.isBoxedPrimitive(Object(BigInt(5))); // Returns true + * ``` + * @since v10.11.0 + */ + function isBoxedPrimitive(object: unknown): object is String | Number | BigInt | Boolean | Symbol; + /** + * Returns `true` if the value is a built-in [`DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) instance. + * + * ```js + * const ab = new ArrayBuffer(20); + * util.types.isDataView(new DataView(ab)); // Returns true + * util.types.isDataView(new Float64Array()); // Returns false + * ``` + * + * See also [`ArrayBuffer.isView()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/isView). + * @since v10.0.0 + */ + function isDataView(object: unknown): object is DataView; + /** + * Returns `true` if the value is a built-in [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) instance. + * + * ```js + * util.types.isDate(new Date()); // Returns true + * ``` + * @since v10.0.0 + */ + function isDate(object: unknown): object is Date; + /** + * Returns `true` if the value is a native `External` value. + * + * A native `External` value is a special type of object that contains a + * raw C++ pointer (`void*`) for access from native code, and has no other + * properties. Such objects are created either by Node.js internals or native + * addons. In JavaScript, they are + * [frozen](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) objects with a + * `null` prototype. + * + * ```c + * #include + * #include + * napi_value result; + * static napi_value MyNapi(napi_env env, napi_callback_info info) { + * int* raw = (int*) malloc(1024); + * napi_status status = napi_create_external(env, (void*) raw, NULL, NULL, &result); + * if (status != napi_ok) { + * napi_throw_error(env, NULL, "napi_create_external failed"); + * return NULL; + * } + * return result; + * } + * ... + * DECLARE_NAPI_PROPERTY("myNapi", MyNapi) + * ... + * ``` + * + * ```js + * import native from 'napi_addon.node'; + * import { types } from 'node:util'; + * + * const data = native.myNapi(); + * types.isExternal(data); // returns true + * types.isExternal(0); // returns false + * types.isExternal(new String('foo')); // returns false + * ``` + * + * For further information on `napi_create_external`, refer to + * [`napi_create_external()`](https://nodejs.org/docs/latest-v22.x/api/n-api.html#napi_create_external). + * @since v10.0.0 + */ + function isExternal(object: unknown): boolean; + /** + * Returns `true` if the value is a built-in `Float16Array` instance. + * + * ```js + * util.types.isFloat16Array(new ArrayBuffer()); // Returns false + * util.types.isFloat16Array(new Float16Array()); // Returns true + * util.types.isFloat16Array(new Float32Array()); // Returns false + * ``` + * @since v22.16.0 + */ + // This does NOT return a type predicate in v22.x. + // The Float16Array feature does not yet exist in this version of Node.js. + function isFloat16Array(object: unknown): boolean; + /** + * Returns `true` if the value is a built-in [`Float32Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array) instance. + * + * ```js + * util.types.isFloat32Array(new ArrayBuffer()); // Returns false + * util.types.isFloat32Array(new Float32Array()); // Returns true + * util.types.isFloat32Array(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isFloat32Array(object: unknown): object is Float32Array; + /** + * Returns `true` if the value is a built-in [`Float64Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float64Array) instance. + * + * ```js + * util.types.isFloat64Array(new ArrayBuffer()); // Returns false + * util.types.isFloat64Array(new Uint8Array()); // Returns false + * util.types.isFloat64Array(new Float64Array()); // Returns true + * ``` + * @since v10.0.0 + */ + function isFloat64Array(object: unknown): object is Float64Array; + /** + * Returns `true` if the value is a generator function. + * This only reports back what the JavaScript engine is seeing; + * in particular, the return value may not match the original source code if + * a transpilation tool was used. + * + * ```js + * util.types.isGeneratorFunction(function foo() {}); // Returns false + * util.types.isGeneratorFunction(function* foo() {}); // Returns true + * ``` + * @since v10.0.0 + */ + function isGeneratorFunction(object: unknown): object is GeneratorFunction; + /** + * Returns `true` if the value is a generator object as returned from a + * built-in generator function. + * This only reports back what the JavaScript engine is seeing; + * in particular, the return value may not match the original source code if + * a transpilation tool was used. + * + * ```js + * function* foo() {} + * const generator = foo(); + * util.types.isGeneratorObject(generator); // Returns true + * ``` + * @since v10.0.0 + */ + function isGeneratorObject(object: unknown): object is Generator; + /** + * Returns `true` if the value is a built-in [`Int8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array) instance. + * + * ```js + * util.types.isInt8Array(new ArrayBuffer()); // Returns false + * util.types.isInt8Array(new Int8Array()); // Returns true + * util.types.isInt8Array(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isInt8Array(object: unknown): object is Int8Array; + /** + * Returns `true` if the value is a built-in [`Int16Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array) instance. + * + * ```js + * util.types.isInt16Array(new ArrayBuffer()); // Returns false + * util.types.isInt16Array(new Int16Array()); // Returns true + * util.types.isInt16Array(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isInt16Array(object: unknown): object is Int16Array; + /** + * Returns `true` if the value is a built-in [`Int32Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array) instance. + * + * ```js + * util.types.isInt32Array(new ArrayBuffer()); // Returns false + * util.types.isInt32Array(new Int32Array()); // Returns true + * util.types.isInt32Array(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isInt32Array(object: unknown): object is Int32Array; + /** + * Returns `true` if the value is a built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) instance. + * + * ```js + * util.types.isMap(new Map()); // Returns true + * ``` + * @since v10.0.0 + */ + function isMap( + object: T | {}, + ): object is T extends ReadonlyMap ? (unknown extends T ? never : ReadonlyMap) + : Map; + /** + * Returns `true` if the value is an iterator returned for a built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) instance. + * + * ```js + * const map = new Map(); + * util.types.isMapIterator(map.keys()); // Returns true + * util.types.isMapIterator(map.values()); // Returns true + * util.types.isMapIterator(map.entries()); // Returns true + * util.types.isMapIterator(map[Symbol.iterator]()); // Returns true + * ``` + * @since v10.0.0 + */ + function isMapIterator(object: unknown): boolean; + /** + * Returns `true` if the value is an instance of a [Module Namespace Object](https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects). + * + * ```js + * import * as ns from './a.js'; + * + * util.types.isModuleNamespaceObject(ns); // Returns true + * ``` + * @since v10.0.0 + */ + function isModuleNamespaceObject(value: unknown): boolean; + /** + * Returns `true` if the value was returned by the constructor of a + * [built-in `Error` type](https://tc39.es/ecma262/#sec-error-objects). + * + * ```js + * console.log(util.types.isNativeError(new Error())); // true + * console.log(util.types.isNativeError(new TypeError())); // true + * console.log(util.types.isNativeError(new RangeError())); // true + * ``` + * + * Subclasses of the native error types are also native errors: + * + * ```js + * class MyError extends Error {} + * console.log(util.types.isNativeError(new MyError())); // true + * ``` + * + * A value being `instanceof` a native error class is not equivalent to `isNativeError()` + * returning `true` for that value. `isNativeError()` returns `true` for errors + * which come from a different [realm](https://tc39.es/ecma262/#realm) while `instanceof Error` returns `false` + * for these errors: + * + * ```js + * import { createContext, runInContext } from 'node:vm'; + * import { types } from 'node:util'; + * + * const context = createContext({}); + * const myError = runInContext('new Error()', context); + * console.log(types.isNativeError(myError)); // true + * console.log(myError instanceof Error); // false + * ``` + * + * Conversely, `isNativeError()` returns `false` for all objects which were not + * returned by the constructor of a native error. That includes values + * which are `instanceof` native errors: + * + * ```js + * const myError = { __proto__: Error.prototype }; + * console.log(util.types.isNativeError(myError)); // false + * console.log(myError instanceof Error); // true + * ``` + * @since v10.0.0 + */ + function isNativeError(object: unknown): object is Error; + /** + * Returns `true` if the value is a number object, e.g. created + * by `new Number()`. + * + * ```js + * util.types.isNumberObject(0); // Returns false + * util.types.isNumberObject(new Number(0)); // Returns true + * ``` + * @since v10.0.0 + */ + function isNumberObject(object: unknown): object is Number; + /** + * Returns `true` if the value is a built-in [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + * + * ```js + * util.types.isPromise(Promise.resolve(42)); // Returns true + * ``` + * @since v10.0.0 + */ + function isPromise(object: unknown): object is Promise; + /** + * Returns `true` if the value is a [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) instance. + * + * ```js + * const target = {}; + * const proxy = new Proxy(target, {}); + * util.types.isProxy(target); // Returns false + * util.types.isProxy(proxy); // Returns true + * ``` + * @since v10.0.0 + */ + function isProxy(object: unknown): boolean; + /** + * Returns `true` if the value is a regular expression object. + * + * ```js + * util.types.isRegExp(/abc/); // Returns true + * util.types.isRegExp(new RegExp('abc')); // Returns true + * ``` + * @since v10.0.0 + */ + function isRegExp(object: unknown): object is RegExp; + /** + * Returns `true` if the value is a built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) instance. + * + * ```js + * util.types.isSet(new Set()); // Returns true + * ``` + * @since v10.0.0 + */ + function isSet( + object: T | {}, + ): object is T extends ReadonlySet ? (unknown extends T ? never : ReadonlySet) : Set; + /** + * Returns `true` if the value is an iterator returned for a built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) instance. + * + * ```js + * const set = new Set(); + * util.types.isSetIterator(set.keys()); // Returns true + * util.types.isSetIterator(set.values()); // Returns true + * util.types.isSetIterator(set.entries()); // Returns true + * util.types.isSetIterator(set[Symbol.iterator]()); // Returns true + * ``` + * @since v10.0.0 + */ + function isSetIterator(object: unknown): boolean; + /** + * Returns `true` if the value is a built-in [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) instance. + * This does _not_ include [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) instances. Usually, it is + * desirable to test for both; See `util.types.isAnyArrayBuffer()` for that. + * + * ```js + * util.types.isSharedArrayBuffer(new ArrayBuffer()); // Returns false + * util.types.isSharedArrayBuffer(new SharedArrayBuffer()); // Returns true + * ``` + * @since v10.0.0 + */ + function isSharedArrayBuffer(object: unknown): object is SharedArrayBuffer; + /** + * Returns `true` if the value is a string object, e.g. created + * by `new String()`. + * + * ```js + * util.types.isStringObject('foo'); // Returns false + * util.types.isStringObject(new String('foo')); // Returns true + * ``` + * @since v10.0.0 + */ + function isStringObject(object: unknown): object is String; + /** + * Returns `true` if the value is a symbol object, created + * by calling `Object()` on a `Symbol` primitive. + * + * ```js + * const symbol = Symbol('foo'); + * util.types.isSymbolObject(symbol); // Returns false + * util.types.isSymbolObject(Object(symbol)); // Returns true + * ``` + * @since v10.0.0 + */ + function isSymbolObject(object: unknown): object is Symbol; + /** + * Returns `true` if the value is a built-in [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) instance. + * + * ```js + * util.types.isTypedArray(new ArrayBuffer()); // Returns false + * util.types.isTypedArray(new Uint8Array()); // Returns true + * util.types.isTypedArray(new Float64Array()); // Returns true + * ``` + * + * See also [`ArrayBuffer.isView()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/isView). + * @since v10.0.0 + */ + function isTypedArray(object: unknown): object is NodeJS.TypedArray; + /** + * Returns `true` if the value is a built-in [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) instance. + * + * ```js + * util.types.isUint8Array(new ArrayBuffer()); // Returns false + * util.types.isUint8Array(new Uint8Array()); // Returns true + * util.types.isUint8Array(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isUint8Array(object: unknown): object is Uint8Array; + /** + * Returns `true` if the value is a built-in [`Uint8ClampedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray) instance. + * + * ```js + * util.types.isUint8ClampedArray(new ArrayBuffer()); // Returns false + * util.types.isUint8ClampedArray(new Uint8ClampedArray()); // Returns true + * util.types.isUint8ClampedArray(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isUint8ClampedArray(object: unknown): object is Uint8ClampedArray; + /** + * Returns `true` if the value is a built-in [`Uint16Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array) instance. + * + * ```js + * util.types.isUint16Array(new ArrayBuffer()); // Returns false + * util.types.isUint16Array(new Uint16Array()); // Returns true + * util.types.isUint16Array(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isUint16Array(object: unknown): object is Uint16Array; + /** + * Returns `true` if the value is a built-in [`Uint32Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array) instance. + * + * ```js + * util.types.isUint32Array(new ArrayBuffer()); // Returns false + * util.types.isUint32Array(new Uint32Array()); // Returns true + * util.types.isUint32Array(new Float64Array()); // Returns false + * ``` + * @since v10.0.0 + */ + function isUint32Array(object: unknown): object is Uint32Array; + /** + * Returns `true` if the value is a built-in [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) instance. + * + * ```js + * util.types.isWeakMap(new WeakMap()); // Returns true + * ``` + * @since v10.0.0 + */ + function isWeakMap(object: unknown): object is WeakMap; + /** + * Returns `true` if the value is a built-in [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) instance. + * + * ```js + * util.types.isWeakSet(new WeakSet()); // Returns true + * ``` + * @since v10.0.0 + */ + function isWeakSet(object: unknown): object is WeakSet; + /** + * Returns `true` if `value` is a `KeyObject`, `false` otherwise. + * @since v16.2.0 + */ + function isKeyObject(object: unknown): object is KeyObject; + /** + * Returns `true` if `value` is a `CryptoKey`, `false` otherwise. + * @since v16.2.0 + */ + function isCryptoKey(object: unknown): object is webcrypto.CryptoKey; +} +declare module "node:util" { + export * from "util"; +} +declare module "node:util/types" { + export * from "util/types"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/v8.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/v8.d.ts new file mode 100644 index 00000000..34006cd4 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/v8.d.ts @@ -0,0 +1,920 @@ +/** + * The `node:v8` module exposes APIs that are specific to the version of [V8](https://developers.google.com/v8/) built into the Node.js binary. It can be accessed using: + * + * ```js + * import v8 from 'node:v8'; + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/v8.js) + */ +declare module "v8" { + import { NonSharedBuffer } from "node:buffer"; + import { Readable } from "node:stream"; + interface HeapSpaceInfo { + space_name: string; + space_size: number; + space_used_size: number; + space_available_size: number; + physical_space_size: number; + } + // ** Signifies if the --zap_code_space option is enabled or not. 1 == enabled, 0 == disabled. */ + type DoesZapCodeSpaceFlag = 0 | 1; + interface HeapInfo { + total_heap_size: number; + total_heap_size_executable: number; + total_physical_size: number; + total_available_size: number; + used_heap_size: number; + heap_size_limit: number; + malloced_memory: number; + peak_malloced_memory: number; + does_zap_garbage: DoesZapCodeSpaceFlag; + number_of_native_contexts: number; + number_of_detached_contexts: number; + total_global_handles_size: number; + used_global_handles_size: number; + external_memory: number; + } + interface HeapCodeStatistics { + code_and_metadata_size: number; + bytecode_and_metadata_size: number; + external_script_source_size: number; + } + interface HeapSnapshotOptions { + /** + * If true, expose internals in the heap snapshot. + * @default false + */ + exposeInternals?: boolean | undefined; + /** + * If true, expose numeric values in artificial fields. + * @default false + */ + exposeNumericValues?: boolean | undefined; + } + /** + * Returns an integer representing a version tag derived from the V8 version, + * command-line flags, and detected CPU features. This is useful for determining + * whether a `vm.Script` `cachedData` buffer is compatible with this instance + * of V8. + * + * ```js + * console.log(v8.cachedDataVersionTag()); // 3947234607 + * // The value returned by v8.cachedDataVersionTag() is derived from the V8 + * // version, command-line flags, and detected CPU features. Test that the value + * // does indeed update when flags are toggled. + * v8.setFlagsFromString('--allow_natives_syntax'); + * console.log(v8.cachedDataVersionTag()); // 183726201 + * ``` + * @since v8.0.0 + */ + function cachedDataVersionTag(): number; + /** + * Returns an object with the following properties: + * + * `does_zap_garbage` is a 0/1 boolean, which signifies whether the `--zap_code_space` option is enabled or not. This makes V8 overwrite heap + * garbage with a bit pattern. The RSS footprint (resident set size) gets bigger + * because it continuously touches all heap pages and that makes them less likely + * to get swapped out by the operating system. + * + * `number_of_native_contexts` The value of native\_context is the number of the + * top-level contexts currently active. Increase of this number over time indicates + * a memory leak. + * + * `number_of_detached_contexts` The value of detached\_context is the number + * of contexts that were detached and not yet garbage collected. This number + * being non-zero indicates a potential memory leak. + * + * `total_global_handles_size` The value of total\_global\_handles\_size is the + * total memory size of V8 global handles. + * + * `used_global_handles_size` The value of used\_global\_handles\_size is the + * used memory size of V8 global handles. + * + * `external_memory` The value of external\_memory is the memory size of array + * buffers and external strings. + * + * ```js + * { + * total_heap_size: 7326976, + * total_heap_size_executable: 4194304, + * total_physical_size: 7326976, + * total_available_size: 1152656, + * used_heap_size: 3476208, + * heap_size_limit: 1535115264, + * malloced_memory: 16384, + * peak_malloced_memory: 1127496, + * does_zap_garbage: 0, + * number_of_native_contexts: 1, + * number_of_detached_contexts: 0, + * total_global_handles_size: 8192, + * used_global_handles_size: 3296, + * external_memory: 318824 + * } + * ``` + * @since v1.0.0 + */ + function getHeapStatistics(): HeapInfo; + /** + * It returns an object with a structure similar to the + * [`cppgc::HeapStatistics`](https://v8docs.nodesource.com/node-22.4/d7/d51/heap-statistics_8h_source.html) + * object. See the [V8 documentation](https://v8docs.nodesource.com/node-22.4/df/d2f/structcppgc_1_1_heap_statistics.html) + * for more information about the properties of the object. + * + * ```js + * // Detailed + * ({ + * committed_size_bytes: 131072, + * resident_size_bytes: 131072, + * used_size_bytes: 152, + * space_statistics: [ + * { + * name: 'NormalPageSpace0', + * committed_size_bytes: 0, + * resident_size_bytes: 0, + * used_size_bytes: 0, + * page_stats: [{}], + * free_list_stats: {}, + * }, + * { + * name: 'NormalPageSpace1', + * committed_size_bytes: 131072, + * resident_size_bytes: 131072, + * used_size_bytes: 152, + * page_stats: [{}], + * free_list_stats: {}, + * }, + * { + * name: 'NormalPageSpace2', + * committed_size_bytes: 0, + * resident_size_bytes: 0, + * used_size_bytes: 0, + * page_stats: [{}], + * free_list_stats: {}, + * }, + * { + * name: 'NormalPageSpace3', + * committed_size_bytes: 0, + * resident_size_bytes: 0, + * used_size_bytes: 0, + * page_stats: [{}], + * free_list_stats: {}, + * }, + * { + * name: 'LargePageSpace', + * committed_size_bytes: 0, + * resident_size_bytes: 0, + * used_size_bytes: 0, + * page_stats: [{}], + * free_list_stats: {}, + * }, + * ], + * type_names: [], + * detail_level: 'detailed', + * }); + * ``` + * + * ```js + * // Brief + * ({ + * committed_size_bytes: 131072, + * resident_size_bytes: 131072, + * used_size_bytes: 128864, + * space_statistics: [], + * type_names: [], + * detail_level: 'brief', + * }); + * ``` + * @since v22.15.0 + * @param detailLevel **Default:** `'detailed'`. Specifies the level of detail in the returned statistics. + * Accepted values are: + * * `'brief'`: Brief statistics contain only the top-level + * allocated and used + * memory statistics for the entire heap. + * * `'detailed'`: Detailed statistics also contain a break + * down per space and page, as well as freelist statistics + * and object type histograms. + */ + function getCppHeapStatistics(detailLevel?: "brief" | "detailed"): object; + /** + * Returns statistics about the V8 heap spaces, i.e. the segments which make up + * the V8 heap. Neither the ordering of heap spaces, nor the availability of a + * heap space can be guaranteed as the statistics are provided via the + * V8 [`GetHeapSpaceStatistics`](https://v8docs.nodesource.com/node-13.2/d5/dda/classv8_1_1_isolate.html#ac673576f24fdc7a33378f8f57e1d13a4) function and may change from one V8 version to the + * next. + * + * The value returned is an array of objects containing the following properties: + * + * ```json + * [ + * { + * "space_name": "new_space", + * "space_size": 2063872, + * "space_used_size": 951112, + * "space_available_size": 80824, + * "physical_space_size": 2063872 + * }, + * { + * "space_name": "old_space", + * "space_size": 3090560, + * "space_used_size": 2493792, + * "space_available_size": 0, + * "physical_space_size": 3090560 + * }, + * { + * "space_name": "code_space", + * "space_size": 1260160, + * "space_used_size": 644256, + * "space_available_size": 960, + * "physical_space_size": 1260160 + * }, + * { + * "space_name": "map_space", + * "space_size": 1094160, + * "space_used_size": 201608, + * "space_available_size": 0, + * "physical_space_size": 1094160 + * }, + * { + * "space_name": "large_object_space", + * "space_size": 0, + * "space_used_size": 0, + * "space_available_size": 1490980608, + * "physical_space_size": 0 + * } + * ] + * ``` + * @since v6.0.0 + */ + function getHeapSpaceStatistics(): HeapSpaceInfo[]; + /** + * The `v8.setFlagsFromString()` method can be used to programmatically set + * V8 command-line flags. This method should be used with care. Changing settings + * after the VM has started may result in unpredictable behavior, including + * crashes and data loss; or it may simply do nothing. + * + * The V8 options available for a version of Node.js may be determined by running `node --v8-options`. + * + * Usage: + * + * ```js + * // Print GC events to stdout for one minute. + * import v8 from 'node:v8'; + * v8.setFlagsFromString('--trace_gc'); + * setTimeout(() => { v8.setFlagsFromString('--notrace_gc'); }, 60e3); + * ``` + * @since v1.0.0 + */ + function setFlagsFromString(flags: string): void; + /** + * This is similar to the [`queryObjects()` console API](https://developer.chrome.com/docs/devtools/console/utilities#queryObjects-function) + * provided by the Chromium DevTools console. It can be used to search for objects that have the matching constructor on its prototype chain + * in the heap after a full garbage collection, which can be useful for memory leak regression tests. To avoid surprising results, users should + * avoid using this API on constructors whose implementation they don't control, or on constructors that can be invoked by other parties in the + * application. + * + * To avoid accidental leaks, this API does not return raw references to the objects found. By default, it returns the count of the objects + * found. If `options.format` is `'summary'`, it returns an array containing brief string representations for each object. The visibility provided + * in this API is similar to what the heap snapshot provides, while users can save the cost of serialization and parsing and directly filter the + * target objects during the search. + * + * Only objects created in the current execution context are included in the results. + * + * ```js + * import { queryObjects } from 'node:v8'; + * class A { foo = 'bar'; } + * console.log(queryObjects(A)); // 0 + * const a = new A(); + * console.log(queryObjects(A)); // 1 + * // [ "A { foo: 'bar' }" ] + * console.log(queryObjects(A, { format: 'summary' })); + * + * class B extends A { bar = 'qux'; } + * const b = new B(); + * console.log(queryObjects(B)); // 1 + * // [ "B { foo: 'bar', bar: 'qux' }" ] + * console.log(queryObjects(B, { format: 'summary' })); + * + * // Note that, when there are child classes inheriting from a constructor, + * // the constructor also shows up in the prototype chain of the child + * // classes's prototoype, so the child classes's prototoype would also be + * // included in the result. + * console.log(queryObjects(A)); // 3 + * // [ "B { foo: 'bar', bar: 'qux' }", 'A {}', "A { foo: 'bar' }" ] + * console.log(queryObjects(A, { format: 'summary' })); + * ``` + * @param ctor The constructor that can be used to search on the prototype chain in order to filter target objects in the heap. + * @since v20.13.0 + * @experimental + */ + function queryObjects(ctor: Function): number | string[]; + function queryObjects(ctor: Function, options: { format: "count" }): number; + function queryObjects(ctor: Function, options: { format: "summary" }): string[]; + /** + * Generates a snapshot of the current V8 heap and returns a Readable + * Stream that may be used to read the JSON serialized representation. + * This JSON stream format is intended to be used with tools such as + * Chrome DevTools. The JSON schema is undocumented and specific to the + * V8 engine. Therefore, the schema may change from one version of V8 to the next. + * + * Creating a heap snapshot requires memory about twice the size of the heap at + * the time the snapshot is created. This results in the risk of OOM killers + * terminating the process. + * + * Generating a snapshot is a synchronous operation which blocks the event loop + * for a duration depending on the heap size. + * + * ```js + * // Print heap snapshot to the console + * import v8 from 'node:v8'; + * const stream = v8.getHeapSnapshot(); + * stream.pipe(process.stdout); + * ``` + * @since v11.13.0 + * @return A Readable containing the V8 heap snapshot. + */ + function getHeapSnapshot(options?: HeapSnapshotOptions): Readable; + /** + * Generates a snapshot of the current V8 heap and writes it to a JSON + * file. This file is intended to be used with tools such as Chrome + * DevTools. The JSON schema is undocumented and specific to the V8 + * engine, and may change from one version of V8 to the next. + * + * A heap snapshot is specific to a single V8 isolate. When using `worker threads`, a heap snapshot generated from the main thread will + * not contain any information about the workers, and vice versa. + * + * Creating a heap snapshot requires memory about twice the size of the heap at + * the time the snapshot is created. This results in the risk of OOM killers + * terminating the process. + * + * Generating a snapshot is a synchronous operation which blocks the event loop + * for a duration depending on the heap size. + * + * ```js + * import { writeHeapSnapshot } from 'node:v8'; + * import { + * Worker, + * isMainThread, + * parentPort, + * } from 'node:worker_threads'; + * + * if (isMainThread) { + * const worker = new Worker(__filename); + * + * worker.once('message', (filename) => { + * console.log(`worker heapdump: ${filename}`); + * // Now get a heapdump for the main thread. + * console.log(`main thread heapdump: ${writeHeapSnapshot()}`); + * }); + * + * // Tell the worker to create a heapdump. + * worker.postMessage('heapdump'); + * } else { + * parentPort.once('message', (message) => { + * if (message === 'heapdump') { + * // Generate a heapdump for the worker + * // and return the filename to the parent. + * parentPort.postMessage(writeHeapSnapshot()); + * } + * }); + * } + * ``` + * @since v11.13.0 + * @param filename The file path where the V8 heap snapshot is to be saved. If not specified, a file name with the pattern `'Heap-${yyyymmdd}-${hhmmss}-${pid}-${thread_id}.heapsnapshot'` will be + * generated, where `{pid}` will be the PID of the Node.js process, `{thread_id}` will be `0` when `writeHeapSnapshot()` is called from the main Node.js thread or the id of a + * worker thread. + * @return The filename where the snapshot was saved. + */ + function writeHeapSnapshot(filename?: string, options?: HeapSnapshotOptions): string; + /** + * Get statistics about code and its metadata in the heap, see + * V8 [`GetHeapCodeAndMetadataStatistics`](https://v8docs.nodesource.com/node-13.2/d5/dda/classv8_1_1_isolate.html#a6079122af17612ef54ef3348ce170866) API. Returns an object with the + * following properties: + * + * ```js + * { + * code_and_metadata_size: 212208, + * bytecode_and_metadata_size: 161368, + * external_script_source_size: 1410794, + * cpu_profiler_metadata_size: 0, + * } + * ``` + * @since v12.8.0 + */ + function getHeapCodeStatistics(): HeapCodeStatistics; + /** + * V8 only supports `Latin-1/ISO-8859-1` and `UTF16` as the underlying representation of a string. + * If the `content` uses `Latin-1/ISO-8859-1` as the underlying representation, this function will return true; + * otherwise, it returns false. + * + * If this method returns false, that does not mean that the string contains some characters not in `Latin-1/ISO-8859-1`. + * Sometimes a `Latin-1` string may also be represented as `UTF16`. + * + * ```js + * const { isStringOneByteRepresentation } = require('node:v8'); + * + * const Encoding = { + * latin1: 1, + * utf16le: 2, + * }; + * const buffer = Buffer.alloc(100); + * function writeString(input) { + * if (isStringOneByteRepresentation(input)) { + * buffer.writeUint8(Encoding.latin1); + * buffer.writeUint32LE(input.length, 1); + * buffer.write(input, 5, 'latin1'); + * } else { + * buffer.writeUint8(Encoding.utf16le); + * buffer.writeUint32LE(input.length * 2, 1); + * buffer.write(input, 5, 'utf16le'); + * } + * } + * writeString('hello'); + * writeString('你好'); + * ``` + * @since v22.15.0 + */ + function isStringOneByteRepresentation(content: string): boolean; + /** + * @since v8.0.0 + */ + class Serializer { + /** + * Writes out a header, which includes the serialization format version. + */ + writeHeader(): void; + /** + * Serializes a JavaScript value and adds the serialized representation to the + * internal buffer. + * + * This throws an error if `value` cannot be serialized. + */ + writeValue(val: any): boolean; + /** + * Returns the stored internal buffer. This serializer should not be used once + * the buffer is released. Calling this method results in undefined behavior + * if a previous write has failed. + */ + releaseBuffer(): NonSharedBuffer; + /** + * Marks an `ArrayBuffer` as having its contents transferred out of band. + * Pass the corresponding `ArrayBuffer` in the deserializing context to `deserializer.transferArrayBuffer()`. + * @param id A 32-bit unsigned integer. + * @param arrayBuffer An `ArrayBuffer` instance. + */ + transferArrayBuffer(id: number, arrayBuffer: ArrayBuffer): void; + /** + * Write a raw 32-bit unsigned integer. + * For use inside of a custom `serializer._writeHostObject()`. + */ + writeUint32(value: number): void; + /** + * Write a raw 64-bit unsigned integer, split into high and low 32-bit parts. + * For use inside of a custom `serializer._writeHostObject()`. + */ + writeUint64(hi: number, lo: number): void; + /** + * Write a JS `number` value. + * For use inside of a custom `serializer._writeHostObject()`. + */ + writeDouble(value: number): void; + /** + * Write raw bytes into the serializer's internal buffer. The deserializer + * will require a way to compute the length of the buffer. + * For use inside of a custom `serializer._writeHostObject()`. + */ + writeRawBytes(buffer: NodeJS.ArrayBufferView): void; + } + /** + * A subclass of `Serializer` that serializes `TypedArray`(in particular `Buffer`) and `DataView` objects as host objects, and only + * stores the part of their underlying `ArrayBuffer`s that they are referring to. + * @since v8.0.0 + */ + class DefaultSerializer extends Serializer {} + /** + * @since v8.0.0 + */ + class Deserializer { + constructor(data: NodeJS.TypedArray); + /** + * Reads and validates a header (including the format version). + * May, for example, reject an invalid or unsupported wire format. In that case, + * an `Error` is thrown. + */ + readHeader(): boolean; + /** + * Deserializes a JavaScript value from the buffer and returns it. + */ + readValue(): any; + /** + * Marks an `ArrayBuffer` as having its contents transferred out of band. + * Pass the corresponding `ArrayBuffer` in the serializing context to `serializer.transferArrayBuffer()` (or return the `id` from `serializer._getSharedArrayBufferId()` in the case of + * `SharedArrayBuffer`s). + * @param id A 32-bit unsigned integer. + * @param arrayBuffer An `ArrayBuffer` instance. + */ + transferArrayBuffer(id: number, arrayBuffer: ArrayBuffer): void; + /** + * Reads the underlying wire format version. Likely mostly to be useful to + * legacy code reading old wire format versions. May not be called before `.readHeader()`. + */ + getWireFormatVersion(): number; + /** + * Read a raw 32-bit unsigned integer and return it. + * For use inside of a custom `deserializer._readHostObject()`. + */ + readUint32(): number; + /** + * Read a raw 64-bit unsigned integer and return it as an array `[hi, lo]` with two 32-bit unsigned integer entries. + * For use inside of a custom `deserializer._readHostObject()`. + */ + readUint64(): [number, number]; + /** + * Read a JS `number` value. + * For use inside of a custom `deserializer._readHostObject()`. + */ + readDouble(): number; + /** + * Read raw bytes from the deserializer's internal buffer. The `length` parameter + * must correspond to the length of the buffer that was passed to `serializer.writeRawBytes()`. + * For use inside of a custom `deserializer._readHostObject()`. + */ + readRawBytes(length: number): Buffer; + } + /** + * A subclass of `Deserializer` corresponding to the format written by `DefaultSerializer`. + * @since v8.0.0 + */ + class DefaultDeserializer extends Deserializer {} + /** + * Uses a `DefaultSerializer` to serialize `value` into a buffer. + * + * `ERR_BUFFER_TOO_LARGE` will be thrown when trying to + * serialize a huge object which requires buffer + * larger than `buffer.constants.MAX_LENGTH`. + * @since v8.0.0 + */ + function serialize(value: any): NonSharedBuffer; + /** + * Uses a `DefaultDeserializer` with default options to read a JS value + * from a buffer. + * @since v8.0.0 + * @param buffer A buffer returned by {@link serialize}. + */ + function deserialize(buffer: NodeJS.ArrayBufferView): any; + /** + * The `v8.takeCoverage()` method allows the user to write the coverage started by `NODE_V8_COVERAGE` to disk on demand. This method can be invoked multiple + * times during the lifetime of the process. Each time the execution counter will + * be reset and a new coverage report will be written to the directory specified + * by `NODE_V8_COVERAGE`. + * + * When the process is about to exit, one last coverage will still be written to + * disk unless {@link stopCoverage} is invoked before the process exits. + * @since v15.1.0, v14.18.0, v12.22.0 + */ + function takeCoverage(): void; + /** + * The `v8.stopCoverage()` method allows the user to stop the coverage collection + * started by `NODE_V8_COVERAGE`, so that V8 can release the execution count + * records and optimize code. This can be used in conjunction with {@link takeCoverage} if the user wants to collect the coverage on demand. + * @since v15.1.0, v14.18.0, v12.22.0 + */ + function stopCoverage(): void; + /** + * The API is a no-op if `--heapsnapshot-near-heap-limit` is already set from the command line or the API is called more than once. + * `limit` must be a positive integer. See [`--heapsnapshot-near-heap-limit`](https://nodejs.org/docs/latest-v22.x/api/cli.html#--heapsnapshot-near-heap-limitmax_count) for more information. + * @since v18.10.0, v16.18.0 + */ + function setHeapSnapshotNearHeapLimit(limit: number): void; + /** + * This API collects GC data in current thread. + * @since v19.6.0, v18.15.0 + */ + class GCProfiler { + /** + * Start collecting GC data. + * @since v19.6.0, v18.15.0 + */ + start(): void; + /** + * Stop collecting GC data and return an object. The content of object + * is as follows. + * + * ```json + * { + * "version": 1, + * "startTime": 1674059033862, + * "statistics": [ + * { + * "gcType": "Scavenge", + * "beforeGC": { + * "heapStatistics": { + * "totalHeapSize": 5005312, + * "totalHeapSizeExecutable": 524288, + * "totalPhysicalSize": 5226496, + * "totalAvailableSize": 4341325216, + * "totalGlobalHandlesSize": 8192, + * "usedGlobalHandlesSize": 2112, + * "usedHeapSize": 4883840, + * "heapSizeLimit": 4345298944, + * "mallocedMemory": 254128, + * "externalMemory": 225138, + * "peakMallocedMemory": 181760 + * }, + * "heapSpaceStatistics": [ + * { + * "spaceName": "read_only_space", + * "spaceSize": 0, + * "spaceUsedSize": 0, + * "spaceAvailableSize": 0, + * "physicalSpaceSize": 0 + * } + * ] + * }, + * "cost": 1574.14, + * "afterGC": { + * "heapStatistics": { + * "totalHeapSize": 6053888, + * "totalHeapSizeExecutable": 524288, + * "totalPhysicalSize": 5500928, + * "totalAvailableSize": 4341101384, + * "totalGlobalHandlesSize": 8192, + * "usedGlobalHandlesSize": 2112, + * "usedHeapSize": 4059096, + * "heapSizeLimit": 4345298944, + * "mallocedMemory": 254128, + * "externalMemory": 225138, + * "peakMallocedMemory": 181760 + * }, + * "heapSpaceStatistics": [ + * { + * "spaceName": "read_only_space", + * "spaceSize": 0, + * "spaceUsedSize": 0, + * "spaceAvailableSize": 0, + * "physicalSpaceSize": 0 + * } + * ] + * } + * } + * ], + * "endTime": 1674059036865 + * } + * ``` + * + * Here's an example. + * + * ```js + * import { GCProfiler } from 'node:v8'; + * const profiler = new GCProfiler(); + * profiler.start(); + * setTimeout(() => { + * console.log(profiler.stop()); + * }, 1000); + * ``` + * @since v19.6.0, v18.15.0 + */ + stop(): GCProfilerResult; + } + interface GCProfilerResult { + version: number; + startTime: number; + endTime: number; + statistics: Array<{ + gcType: string; + cost: number; + beforeGC: { + heapStatistics: HeapStatistics; + heapSpaceStatistics: HeapSpaceStatistics[]; + }; + afterGC: { + heapStatistics: HeapStatistics; + heapSpaceStatistics: HeapSpaceStatistics[]; + }; + }>; + } + interface HeapStatistics { + totalHeapSize: number; + totalHeapSizeExecutable: number; + totalPhysicalSize: number; + totalAvailableSize: number; + totalGlobalHandlesSize: number; + usedGlobalHandlesSize: number; + usedHeapSize: number; + heapSizeLimit: number; + mallocedMemory: number; + externalMemory: number; + peakMallocedMemory: number; + } + interface HeapSpaceStatistics { + spaceName: string; + spaceSize: number; + spaceUsedSize: number; + spaceAvailableSize: number; + physicalSpaceSize: number; + } + /** + * Called when a promise is constructed. This does not mean that corresponding before/after events will occur, only that the possibility exists. This will + * happen if a promise is created without ever getting a continuation. + * @since v17.1.0, v16.14.0 + * @param promise The promise being created. + * @param parent The promise continued from, if applicable. + */ + interface Init { + (promise: Promise, parent: Promise): void; + } + /** + * Called before a promise continuation executes. This can be in the form of `then()`, `catch()`, or `finally()` handlers or an await resuming. + * + * The before callback will be called 0 to N times. The before callback will typically be called 0 times if no continuation was ever made for the promise. + * The before callback may be called many times in the case where many continuations have been made from the same promise. + * @since v17.1.0, v16.14.0 + */ + interface Before { + (promise: Promise): void; + } + /** + * Called immediately after a promise continuation executes. This may be after a `then()`, `catch()`, or `finally()` handler or before an await after another await. + * @since v17.1.0, v16.14.0 + */ + interface After { + (promise: Promise): void; + } + /** + * Called when the promise receives a resolution or rejection value. This may occur synchronously in the case of {@link Promise.resolve()} or + * {@link Promise.reject()}. + * @since v17.1.0, v16.14.0 + */ + interface Settled { + (promise: Promise): void; + } + /** + * Key events in the lifetime of a promise have been categorized into four areas: creation of a promise, before/after a continuation handler is called or + * around an await, and when the promise resolves or rejects. + * + * Because promises are asynchronous resources whose lifecycle is tracked via the promise hooks mechanism, the `init()`, `before()`, `after()`, and + * `settled()` callbacks must not be async functions as they create more promises which would produce an infinite loop. + * @since v17.1.0, v16.14.0 + */ + interface HookCallbacks { + init?: Init; + before?: Before; + after?: After; + settled?: Settled; + } + interface PromiseHooks { + /** + * The `init` hook must be a plain function. Providing an async function will throw as it would produce an infinite microtask loop. + * @since v17.1.0, v16.14.0 + * @param init The {@link Init | `init` callback} to call when a promise is created. + * @return Call to stop the hook. + */ + onInit: (init: Init) => Function; + /** + * The `settled` hook must be a plain function. Providing an async function will throw as it would produce an infinite microtask loop. + * @since v17.1.0, v16.14.0 + * @param settled The {@link Settled | `settled` callback} to call when a promise is created. + * @return Call to stop the hook. + */ + onSettled: (settled: Settled) => Function; + /** + * The `before` hook must be a plain function. Providing an async function will throw as it would produce an infinite microtask loop. + * @since v17.1.0, v16.14.0 + * @param before The {@link Before | `before` callback} to call before a promise continuation executes. + * @return Call to stop the hook. + */ + onBefore: (before: Before) => Function; + /** + * The `after` hook must be a plain function. Providing an async function will throw as it would produce an infinite microtask loop. + * @since v17.1.0, v16.14.0 + * @param after The {@link After | `after` callback} to call after a promise continuation executes. + * @return Call to stop the hook. + */ + onAfter: (after: After) => Function; + /** + * Registers functions to be called for different lifetime events of each promise. + * The callbacks `init()`/`before()`/`after()`/`settled()` are called for the respective events during a promise's lifetime. + * All callbacks are optional. For example, if only promise creation needs to be tracked, then only the init callback needs to be passed. + * The hook callbacks must be plain functions. Providing async functions will throw as it would produce an infinite microtask loop. + * @since v17.1.0, v16.14.0 + * @param callbacks The {@link HookCallbacks | Hook Callbacks} to register + * @return Used for disabling hooks + */ + createHook: (callbacks: HookCallbacks) => Function; + } + /** + * The `promiseHooks` interface can be used to track promise lifecycle events. + * @since v17.1.0, v16.14.0 + */ + const promiseHooks: PromiseHooks; + type StartupSnapshotCallbackFn = (args: any) => any; + /** + * The `v8.startupSnapshot` interface can be used to add serialization and deserialization hooks for custom startup snapshots. + * + * ```bash + * $ node --snapshot-blob snapshot.blob --build-snapshot entry.js + * # This launches a process with the snapshot + * $ node --snapshot-blob snapshot.blob + * ``` + * + * In the example above, `entry.js` can use methods from the `v8.startupSnapshot` interface to specify how to save information for custom objects + * in the snapshot during serialization and how the information can be used to synchronize these objects during deserialization of the snapshot. + * For example, if the `entry.js` contains the following script: + * + * ```js + * 'use strict'; + * + * import fs from 'node:fs'; + * import zlib from 'node:zlib'; + * import path from 'node:path'; + * import assert from 'node:assert'; + * + * import v8 from 'node:v8'; + * + * class BookShelf { + * storage = new Map(); + * + * // Reading a series of files from directory and store them into storage. + * constructor(directory, books) { + * for (const book of books) { + * this.storage.set(book, fs.readFileSync(path.join(directory, book))); + * } + * } + * + * static compressAll(shelf) { + * for (const [ book, content ] of shelf.storage) { + * shelf.storage.set(book, zlib.gzipSync(content)); + * } + * } + * + * static decompressAll(shelf) { + * for (const [ book, content ] of shelf.storage) { + * shelf.storage.set(book, zlib.gunzipSync(content)); + * } + * } + * } + * + * // __dirname here is where the snapshot script is placed + * // during snapshot building time. + * const shelf = new BookShelf(__dirname, [ + * 'book1.en_US.txt', + * 'book1.es_ES.txt', + * 'book2.zh_CN.txt', + * ]); + * + * assert(v8.startupSnapshot.isBuildingSnapshot()); + * // On snapshot serialization, compress the books to reduce size. + * v8.startupSnapshot.addSerializeCallback(BookShelf.compressAll, shelf); + * // On snapshot deserialization, decompress the books. + * v8.startupSnapshot.addDeserializeCallback(BookShelf.decompressAll, shelf); + * v8.startupSnapshot.setDeserializeMainFunction((shelf) => { + * // process.env and process.argv are refreshed during snapshot + * // deserialization. + * const lang = process.env.BOOK_LANG || 'en_US'; + * const book = process.argv[1]; + * const name = `${book}.${lang}.txt`; + * console.log(shelf.storage.get(name)); + * }, shelf); + * ``` + * + * The resulted binary will get print the data deserialized from the snapshot during start up, using the refreshed `process.env` and `process.argv` of the launched process: + * + * ```bash + * $ BOOK_LANG=es_ES node --snapshot-blob snapshot.blob book1 + * # Prints content of book1.es_ES.txt deserialized from the snapshot. + * ``` + * + * Currently the application deserialized from a user-land snapshot cannot be snapshotted again, so these APIs are only available to applications that are not deserialized from a user-land snapshot. + * + * @since v18.6.0, v16.17.0 + */ + namespace startupSnapshot { + /** + * Add a callback that will be called when the Node.js instance is about to get serialized into a snapshot and exit. + * This can be used to release resources that should not or cannot be serialized or to convert user data into a form more suitable for serialization. + * @since v18.6.0, v16.17.0 + */ + function addSerializeCallback(callback: StartupSnapshotCallbackFn, data?: any): void; + /** + * Add a callback that will be called when the Node.js instance is deserialized from a snapshot. + * The `callback` and the `data` (if provided) will be serialized into the snapshot, they can be used to re-initialize the state of the application or + * to re-acquire resources that the application needs when the application is restarted from the snapshot. + * @since v18.6.0, v16.17.0 + */ + function addDeserializeCallback(callback: StartupSnapshotCallbackFn, data?: any): void; + /** + * This sets the entry point of the Node.js application when it is deserialized from a snapshot. This can be called only once in the snapshot building script. + * If called, the deserialized application no longer needs an additional entry point script to start up and will simply invoke the callback along with the deserialized + * data (if provided), otherwise an entry point script still needs to be provided to the deserialized application. + * @since v18.6.0, v16.17.0 + */ + function setDeserializeMainFunction(callback: StartupSnapshotCallbackFn, data?: any): void; + /** + * Returns true if the Node.js instance is run to build a snapshot. + * @since v18.6.0, v16.17.0 + */ + function isBuildingSnapshot(): boolean; + } +} +declare module "node:v8" { + export * from "v8"; +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/vm.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/vm.d.ts new file mode 100644 index 00000000..a2609bf2 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/node/vm.d.ts @@ -0,0 +1,1000 @@ +/** + * The `node:vm` module enables compiling and running code within V8 Virtual + * Machine contexts. + * + * **The `node:vm` module is not a security** + * **mechanism. Do not use it to run untrusted code.** + * + * JavaScript code can be compiled and run immediately or + * compiled, saved, and run later. + * + * A common use case is to run the code in a different V8 Context. This means + * invoked code has a different global object than the invoking code. + * + * One can provide the context by `contextifying` an + * object. The invoked code treats any property in the context like a + * global variable. Any changes to global variables caused by the invoked + * code are reflected in the context object. + * + * ```js + * import vm from 'node:vm'; + * + * const x = 1; + * + * const context = { x: 2 }; + * vm.createContext(context); // Contextify the object. + * + * const code = 'x += 40; var y = 17;'; + * // `x` and `y` are global variables in the context. + * // Initially, x has the value 2 because that is the value of context.x. + * vm.runInContext(code, context); + * + * console.log(context.x); // 42 + * console.log(context.y); // 17 + * + * console.log(x); // 1; y is not defined. + * ``` + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/vm.js) + */ +declare module "vm" { + import { NonSharedBuffer } from "node:buffer"; + import { ImportAttributes } from "node:module"; + interface Context extends NodeJS.Dict {} + interface BaseOptions { + /** + * Specifies the filename used in stack traces produced by this script. + * @default '' + */ + filename?: string | undefined; + /** + * Specifies the line number offset that is displayed in stack traces produced by this script. + * @default 0 + */ + lineOffset?: number | undefined; + /** + * Specifies the column number offset that is displayed in stack traces produced by this script. + * @default 0 + */ + columnOffset?: number | undefined; + } + type DynamicModuleLoader = ( + specifier: string, + referrer: T, + importAttributes: ImportAttributes, + ) => Module | Promise; + interface ScriptOptions extends BaseOptions { + /** + * Provides an optional data with V8's code cache data for the supplied source. + */ + cachedData?: NodeJS.ArrayBufferView | undefined; + /** @deprecated in favor of `script.createCachedData()` */ + produceCachedData?: boolean | undefined; + /** + * Used to specify how the modules should be loaded during the evaluation of this script when `import()` is called. This option is + * part of the experimental modules API. We do not recommend using it in a production environment. For detailed information, see + * [Support of dynamic `import()` in compilation APIs](https://nodejs.org/docs/latest-v22.x/api/vm.html#support-of-dynamic-import-in-compilation-apis). + */ + importModuleDynamically?: + | DynamicModuleLoader +``` + +## Usage + +Import the library in your code, and then pick one of the styles you'd like to use - either `assert`, `expect` or `should`: + +```js +import { assert } from 'chai'; // Using Assert style +import { expect } from 'chai'; // Using Expect style +import { should } from 'chai'; // Using Should style +``` + +### Register the chai testing style globally + +```js +import 'chai/register-assert'; // Using Assert style +import 'chai/register-expect'; // Using Expect style +import 'chai/register-should'; // Using Should style +``` + +### Import assertion styles as local variables + +```js +import { assert } from 'chai'; // Using Assert style +import { expect } from 'chai'; // Using Expect style +import { should } from 'chai'; // Using Should style +should(); // Modifies `Object.prototype` + +import { expect, use } from 'chai'; // Creates local variables `expect` and `use`; useful for plugin use +``` + +### Usage with Mocha + +```bash +mocha spec.js --require chai/register-assert.js # Using Assert style +mocha spec.js --require chai/register-expect.js # Using Expect style +mocha spec.js --require chai/register-should.js # Using Should style +``` + +[Read more about these styles in our docs](http://chaijs.com/guide/styles/). + +## Plugins + +Chai offers a robust Plugin architecture for extending Chai's assertions and interfaces. + +- Need a plugin? View the [official plugin list](http://chaijs.com/plugins). +- Want to build a plugin? Read the [plugin api documentation](http://chaijs.com/guide/plugins/). +- Have a plugin and want it listed? Simply add the following keywords to your package.json: + - `chai-plugin` + - `browser` if your plugin works in the browser as well as Node.js + - `browser-only` if your plugin does not work with Node.js + +### Related Projects + +- [chaijs / chai-docs](https://github.com/chaijs/chai-docs): The chaijs.com website source code. +- [chaijs / assertion-error](https://github.com/chaijs/assertion-error): Custom `Error` constructor thrown upon an assertion failing. +- [chaijs / deep-eql](https://github.com/chaijs/deep-eql): Improved deep equality testing for Node.js and the browser. +- [chaijs / check-error](https://github.com/chaijs/check-error): Error comparison and information related utility for Node.js and the browser. +- [chaijs / loupe](https://github.com/chaijs/loupe): Inspect utility for Node.js and browsers. +- [chaijs / pathval](https://github.com/chaijs/pathval): Object value retrieval given a string path. + +### Contributing + +Thank you very much for considering to contribute! + +Please make sure you follow our [Code Of Conduct](https://github.com/chaijs/chai/blob/master/CODE_OF_CONDUCT.md) and we also strongly recommend reading our [Contributing Guide](https://github.com/chaijs/chai/blob/master/CONTRIBUTING.md). + +Here are a few issues other contributors frequently ran into when opening pull requests: + +- Please do not commit changes to the `chai.js` build. We do it once per release. +- Before pushing your commits, please make sure you [rebase](https://github.com/chaijs/chai/blob/master/CONTRIBUTING.md#pull-requests) them. + +### Contributors + +Please see the full +[Contributors Graph](https://github.com/chaijs/chai/graphs/contributors) for our +list of contributors. + +### Core Contributors + +Feel free to reach out to any of the core contributors with your questions or +concerns. We will do our best to respond in a timely manner. + +[![Keith Cirkel](https://avatars3.githubusercontent.com/u/118266?v=3&s=50)](https://github.com/keithamus) +[![James Garbutt](https://avatars3.githubusercontent.com/u/5677153?v=3&s=50)](https://github.com/43081j) +[![Kristján Oddsson](https://avatars3.githubusercontent.com/u/318208?v=3&s=50)](https://github.com/koddsson) + +### Core Contributor Alumni + +This project would not be what it is without the contributions from our prior +core contributors, for whom we are forever grateful: + +[![Jake Luer](https://avatars3.githubusercontent.com/u/58988?v=3&s=50)](https://github.com/logicalparadox) +[![Veselin Todorov](https://avatars3.githubusercontent.com/u/330048?v=3&s=50)](https://github.com/vesln) +[![Lucas Fernandes da Costa](https://avatars3.githubusercontent.com/u/6868147?v=3&s=50)](https://github.com/lucasfcosta) +[![Grant Snodgrass](https://avatars3.githubusercontent.com/u/17260989?v=3&s=50)](https://github.com/meeber) diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/chai.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/chai.js new file mode 100644 index 00000000..ee01cc9b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/chai.js @@ -0,0 +1 @@ +export * from './index.js'; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/index.js new file mode 100644 index 00000000..d492d453 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/index.js @@ -0,0 +1,4381 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// lib/chai/utils/index.js +var utils_exports = {}; +__export(utils_exports, { + addChainableMethod: () => addChainableMethod, + addLengthGuard: () => addLengthGuard, + addMethod: () => addMethod, + addProperty: () => addProperty, + checkError: () => check_error_exports, + compareByInspect: () => compareByInspect, + eql: () => deep_eql_default, + expectTypes: () => expectTypes, + flag: () => flag, + getActual: () => getActual, + getMessage: () => getMessage2, + getName: () => getName, + getOperator: () => getOperator, + getOwnEnumerableProperties: () => getOwnEnumerableProperties, + getOwnEnumerablePropertySymbols: () => getOwnEnumerablePropertySymbols, + getPathInfo: () => getPathInfo, + hasProperty: () => hasProperty, + inspect: () => inspect2, + isNaN: () => isNaN2, + isNumeric: () => isNumeric, + isProxyEnabled: () => isProxyEnabled, + isRegExp: () => isRegExp2, + objDisplay: () => objDisplay, + overwriteChainableMethod: () => overwriteChainableMethod, + overwriteMethod: () => overwriteMethod, + overwriteProperty: () => overwriteProperty, + proxify: () => proxify, + test: () => test, + transferFlags: () => transferFlags, + type: () => type +}); + +// node_modules/check-error/index.js +var check_error_exports = {}; +__export(check_error_exports, { + compatibleConstructor: () => compatibleConstructor, + compatibleInstance: () => compatibleInstance, + compatibleMessage: () => compatibleMessage, + getConstructorName: () => getConstructorName, + getMessage: () => getMessage +}); +function isErrorInstance(obj) { + return obj instanceof Error || Object.prototype.toString.call(obj) === "[object Error]"; +} +__name(isErrorInstance, "isErrorInstance"); +function isRegExp(obj) { + return Object.prototype.toString.call(obj) === "[object RegExp]"; +} +__name(isRegExp, "isRegExp"); +function compatibleInstance(thrown, errorLike) { + return isErrorInstance(errorLike) && thrown === errorLike; +} +__name(compatibleInstance, "compatibleInstance"); +function compatibleConstructor(thrown, errorLike) { + if (isErrorInstance(errorLike)) { + return thrown.constructor === errorLike.constructor || thrown instanceof errorLike.constructor; + } else if ((typeof errorLike === "object" || typeof errorLike === "function") && errorLike.prototype) { + return thrown.constructor === errorLike || thrown instanceof errorLike; + } + return false; +} +__name(compatibleConstructor, "compatibleConstructor"); +function compatibleMessage(thrown, errMatcher) { + const comparisonString = typeof thrown === "string" ? thrown : thrown.message; + if (isRegExp(errMatcher)) { + return errMatcher.test(comparisonString); + } else if (typeof errMatcher === "string") { + return comparisonString.indexOf(errMatcher) !== -1; + } + return false; +} +__name(compatibleMessage, "compatibleMessage"); +function getConstructorName(errorLike) { + let constructorName = errorLike; + if (isErrorInstance(errorLike)) { + constructorName = errorLike.constructor.name; + } else if (typeof errorLike === "function") { + constructorName = errorLike.name; + if (constructorName === "") { + const newConstructorName = new errorLike().name; + constructorName = newConstructorName || constructorName; + } + } + return constructorName; +} +__name(getConstructorName, "getConstructorName"); +function getMessage(errorLike) { + let msg = ""; + if (errorLike && errorLike.message) { + msg = errorLike.message; + } else if (typeof errorLike === "string") { + msg = errorLike; + } + return msg; +} +__name(getMessage, "getMessage"); + +// lib/chai/utils/flag.js +function flag(obj, key, value) { + let flags = obj.__flags || (obj.__flags = /* @__PURE__ */ Object.create(null)); + if (arguments.length === 3) { + flags[key] = value; + } else { + return flags[key]; + } +} +__name(flag, "flag"); + +// lib/chai/utils/test.js +function test(obj, args) { + let negate = flag(obj, "negate"), expr = args[0]; + return negate ? !expr : expr; +} +__name(test, "test"); + +// lib/chai/utils/type-detect.js +function type(obj) { + if (typeof obj === "undefined") { + return "undefined"; + } + if (obj === null) { + return "null"; + } + const stringTag = obj[Symbol.toStringTag]; + if (typeof stringTag === "string") { + return stringTag; + } + const type3 = Object.prototype.toString.call(obj).slice(8, -1); + return type3; +} +__name(type, "type"); + +// node_modules/assertion-error/index.js +var canElideFrames = "captureStackTrace" in Error; +var AssertionError = class _AssertionError extends Error { + static { + __name(this, "AssertionError"); + } + message; + get name() { + return "AssertionError"; + } + get ok() { + return false; + } + constructor(message = "Unspecified AssertionError", props, ssf) { + super(message); + this.message = message; + if (canElideFrames) { + Error.captureStackTrace(this, ssf || _AssertionError); + } + for (const key in props) { + if (!(key in this)) { + this[key] = props[key]; + } + } + } + toJSON(stack) { + return { + ...this, + name: this.name, + message: this.message, + ok: false, + stack: stack !== false ? this.stack : void 0 + }; + } +}; + +// lib/chai/utils/expectTypes.js +function expectTypes(obj, types) { + let flagMsg = flag(obj, "message"); + let ssfi = flag(obj, "ssfi"); + flagMsg = flagMsg ? flagMsg + ": " : ""; + obj = flag(obj, "object"); + types = types.map(function(t) { + return t.toLowerCase(); + }); + types.sort(); + let str = types.map(function(t, index) { + let art = ~["a", "e", "i", "o", "u"].indexOf(t.charAt(0)) ? "an" : "a"; + let or = types.length > 1 && index === types.length - 1 ? "or " : ""; + return or + art + " " + t; + }).join(", "); + let objType = type(obj).toLowerCase(); + if (!types.some(function(expected) { + return objType === expected; + })) { + throw new AssertionError( + flagMsg + "object tested must be " + str + ", but " + objType + " given", + void 0, + ssfi + ); + } +} +__name(expectTypes, "expectTypes"); + +// lib/chai/utils/getActual.js +function getActual(obj, args) { + return args.length > 4 ? args[4] : obj._obj; +} +__name(getActual, "getActual"); + +// node_modules/loupe/lib/helpers.js +var ansiColors = { + bold: ["1", "22"], + dim: ["2", "22"], + italic: ["3", "23"], + underline: ["4", "24"], + // 5 & 6 are blinking + inverse: ["7", "27"], + hidden: ["8", "28"], + strike: ["9", "29"], + // 10-20 are fonts + // 21-29 are resets for 1-9 + black: ["30", "39"], + red: ["31", "39"], + green: ["32", "39"], + yellow: ["33", "39"], + blue: ["34", "39"], + magenta: ["35", "39"], + cyan: ["36", "39"], + white: ["37", "39"], + brightblack: ["30;1", "39"], + brightred: ["31;1", "39"], + brightgreen: ["32;1", "39"], + brightyellow: ["33;1", "39"], + brightblue: ["34;1", "39"], + brightmagenta: ["35;1", "39"], + brightcyan: ["36;1", "39"], + brightwhite: ["37;1", "39"], + grey: ["90", "39"] +}; +var styles = { + special: "cyan", + number: "yellow", + bigint: "yellow", + boolean: "yellow", + undefined: "grey", + null: "bold", + string: "green", + symbol: "green", + date: "magenta", + regexp: "red" +}; +var truncator = "\u2026"; +function colorise(value, styleType) { + const color = ansiColors[styles[styleType]] || ansiColors[styleType] || ""; + if (!color) { + return String(value); + } + return `\x1B[${color[0]}m${String(value)}\x1B[${color[1]}m`; +} +__name(colorise, "colorise"); +function normaliseOptions({ + showHidden = false, + depth = 2, + colors = false, + customInspect = true, + showProxy = false, + maxArrayLength = Infinity, + breakLength = Infinity, + seen = [], + // eslint-disable-next-line no-shadow + truncate: truncate2 = Infinity, + stylize = String +} = {}, inspect3) { + const options = { + showHidden: Boolean(showHidden), + depth: Number(depth), + colors: Boolean(colors), + customInspect: Boolean(customInspect), + showProxy: Boolean(showProxy), + maxArrayLength: Number(maxArrayLength), + breakLength: Number(breakLength), + truncate: Number(truncate2), + seen, + inspect: inspect3, + stylize + }; + if (options.colors) { + options.stylize = colorise; + } + return options; +} +__name(normaliseOptions, "normaliseOptions"); +function isHighSurrogate(char) { + return char >= "\uD800" && char <= "\uDBFF"; +} +__name(isHighSurrogate, "isHighSurrogate"); +function truncate(string, length, tail = truncator) { + string = String(string); + const tailLength = tail.length; + const stringLength = string.length; + if (tailLength > length && stringLength > tailLength) { + return tail; + } + if (stringLength > length && stringLength > tailLength) { + let end = length - tailLength; + if (end > 0 && isHighSurrogate(string[end - 1])) { + end = end - 1; + } + return `${string.slice(0, end)}${tail}`; + } + return string; +} +__name(truncate, "truncate"); +function inspectList(list, options, inspectItem, separator = ", ") { + inspectItem = inspectItem || options.inspect; + const size = list.length; + if (size === 0) + return ""; + const originalLength = options.truncate; + let output = ""; + let peek = ""; + let truncated = ""; + for (let i = 0; i < size; i += 1) { + const last = i + 1 === list.length; + const secondToLast = i + 2 === list.length; + truncated = `${truncator}(${list.length - i})`; + const value = list[i]; + options.truncate = originalLength - output.length - (last ? 0 : separator.length); + const string = peek || inspectItem(value, options) + (last ? "" : separator); + const nextLength = output.length + string.length; + const truncatedLength = nextLength + truncated.length; + if (last && nextLength > originalLength && output.length + truncated.length <= originalLength) { + break; + } + if (!last && !secondToLast && truncatedLength > originalLength) { + break; + } + peek = last ? "" : inspectItem(list[i + 1], options) + (secondToLast ? "" : separator); + if (!last && secondToLast && truncatedLength > originalLength && nextLength + peek.length > originalLength) { + break; + } + output += string; + if (!last && !secondToLast && nextLength + peek.length >= originalLength) { + truncated = `${truncator}(${list.length - i - 1})`; + break; + } + truncated = ""; + } + return `${output}${truncated}`; +} +__name(inspectList, "inspectList"); +function quoteComplexKey(key) { + if (key.match(/^[a-zA-Z_][a-zA-Z_0-9]*$/)) { + return key; + } + return JSON.stringify(key).replace(/'/g, "\\'").replace(/\\"/g, '"').replace(/(^"|"$)/g, "'"); +} +__name(quoteComplexKey, "quoteComplexKey"); +function inspectProperty([key, value], options) { + options.truncate -= 2; + if (typeof key === "string") { + key = quoteComplexKey(key); + } else if (typeof key !== "number") { + key = `[${options.inspect(key, options)}]`; + } + options.truncate -= key.length; + value = options.inspect(value, options); + return `${key}: ${value}`; +} +__name(inspectProperty, "inspectProperty"); + +// node_modules/loupe/lib/array.js +function inspectArray(array, options) { + const nonIndexProperties = Object.keys(array).slice(array.length); + if (!array.length && !nonIndexProperties.length) + return "[]"; + options.truncate -= 4; + const listContents = inspectList(array, options); + options.truncate -= listContents.length; + let propertyContents = ""; + if (nonIndexProperties.length) { + propertyContents = inspectList(nonIndexProperties.map((key) => [key, array[key]]), options, inspectProperty); + } + return `[ ${listContents}${propertyContents ? `, ${propertyContents}` : ""} ]`; +} +__name(inspectArray, "inspectArray"); + +// node_modules/loupe/lib/typedarray.js +var getArrayName = /* @__PURE__ */ __name((array) => { + if (typeof Buffer === "function" && array instanceof Buffer) { + return "Buffer"; + } + if (array[Symbol.toStringTag]) { + return array[Symbol.toStringTag]; + } + return array.constructor.name; +}, "getArrayName"); +function inspectTypedArray(array, options) { + const name = getArrayName(array); + options.truncate -= name.length + 4; + const nonIndexProperties = Object.keys(array).slice(array.length); + if (!array.length && !nonIndexProperties.length) + return `${name}[]`; + let output = ""; + for (let i = 0; i < array.length; i++) { + const string = `${options.stylize(truncate(array[i], options.truncate), "number")}${i === array.length - 1 ? "" : ", "}`; + options.truncate -= string.length; + if (array[i] !== array.length && options.truncate <= 3) { + output += `${truncator}(${array.length - array[i] + 1})`; + break; + } + output += string; + } + let propertyContents = ""; + if (nonIndexProperties.length) { + propertyContents = inspectList(nonIndexProperties.map((key) => [key, array[key]]), options, inspectProperty); + } + return `${name}[ ${output}${propertyContents ? `, ${propertyContents}` : ""} ]`; +} +__name(inspectTypedArray, "inspectTypedArray"); + +// node_modules/loupe/lib/date.js +function inspectDate(dateObject, options) { + const stringRepresentation = dateObject.toJSON(); + if (stringRepresentation === null) { + return "Invalid Date"; + } + const split = stringRepresentation.split("T"); + const date = split[0]; + return options.stylize(`${date}T${truncate(split[1], options.truncate - date.length - 1)}`, "date"); +} +__name(inspectDate, "inspectDate"); + +// node_modules/loupe/lib/function.js +function inspectFunction(func, options) { + const functionType = func[Symbol.toStringTag] || "Function"; + const name = func.name; + if (!name) { + return options.stylize(`[${functionType}]`, "special"); + } + return options.stylize(`[${functionType} ${truncate(name, options.truncate - 11)}]`, "special"); +} +__name(inspectFunction, "inspectFunction"); + +// node_modules/loupe/lib/map.js +function inspectMapEntry([key, value], options) { + options.truncate -= 4; + key = options.inspect(key, options); + options.truncate -= key.length; + value = options.inspect(value, options); + return `${key} => ${value}`; +} +__name(inspectMapEntry, "inspectMapEntry"); +function mapToEntries(map) { + const entries = []; + map.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +} +__name(mapToEntries, "mapToEntries"); +function inspectMap(map, options) { + if (map.size === 0) + return "Map{}"; + options.truncate -= 7; + return `Map{ ${inspectList(mapToEntries(map), options, inspectMapEntry)} }`; +} +__name(inspectMap, "inspectMap"); + +// node_modules/loupe/lib/number.js +var isNaN = Number.isNaN || ((i) => i !== i); +function inspectNumber(number, options) { + if (isNaN(number)) { + return options.stylize("NaN", "number"); + } + if (number === Infinity) { + return options.stylize("Infinity", "number"); + } + if (number === -Infinity) { + return options.stylize("-Infinity", "number"); + } + if (number === 0) { + return options.stylize(1 / number === Infinity ? "+0" : "-0", "number"); + } + return options.stylize(truncate(String(number), options.truncate), "number"); +} +__name(inspectNumber, "inspectNumber"); + +// node_modules/loupe/lib/bigint.js +function inspectBigInt(number, options) { + let nums = truncate(number.toString(), options.truncate - 1); + if (nums !== truncator) + nums += "n"; + return options.stylize(nums, "bigint"); +} +__name(inspectBigInt, "inspectBigInt"); + +// node_modules/loupe/lib/regexp.js +function inspectRegExp(value, options) { + const flags = value.toString().split("/")[2]; + const sourceLength = options.truncate - (2 + flags.length); + const source = value.source; + return options.stylize(`/${truncate(source, sourceLength)}/${flags}`, "regexp"); +} +__name(inspectRegExp, "inspectRegExp"); + +// node_modules/loupe/lib/set.js +function arrayFromSet(set2) { + const values = []; + set2.forEach((value) => { + values.push(value); + }); + return values; +} +__name(arrayFromSet, "arrayFromSet"); +function inspectSet(set2, options) { + if (set2.size === 0) + return "Set{}"; + options.truncate -= 7; + return `Set{ ${inspectList(arrayFromSet(set2), options)} }`; +} +__name(inspectSet, "inspectSet"); + +// node_modules/loupe/lib/string.js +var stringEscapeChars = new RegExp("['\\u0000-\\u001f\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]", "g"); +var escapeCharacters = { + "\b": "\\b", + " ": "\\t", + "\n": "\\n", + "\f": "\\f", + "\r": "\\r", + "'": "\\'", + "\\": "\\\\" +}; +var hex = 16; +var unicodeLength = 4; +function escape(char) { + return escapeCharacters[char] || `\\u${`0000${char.charCodeAt(0).toString(hex)}`.slice(-unicodeLength)}`; +} +__name(escape, "escape"); +function inspectString(string, options) { + if (stringEscapeChars.test(string)) { + string = string.replace(stringEscapeChars, escape); + } + return options.stylize(`'${truncate(string, options.truncate - 2)}'`, "string"); +} +__name(inspectString, "inspectString"); + +// node_modules/loupe/lib/symbol.js +function inspectSymbol(value) { + if ("description" in Symbol.prototype) { + return value.description ? `Symbol(${value.description})` : "Symbol()"; + } + return value.toString(); +} +__name(inspectSymbol, "inspectSymbol"); + +// node_modules/loupe/lib/promise.js +var getPromiseValue = /* @__PURE__ */ __name(() => "Promise{\u2026}", "getPromiseValue"); +var promise_default = getPromiseValue; + +// node_modules/loupe/lib/object.js +function inspectObject(object, options) { + const properties = Object.getOwnPropertyNames(object); + const symbols = Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(object) : []; + if (properties.length === 0 && symbols.length === 0) { + return "{}"; + } + options.truncate -= 4; + options.seen = options.seen || []; + if (options.seen.includes(object)) { + return "[Circular]"; + } + options.seen.push(object); + const propertyContents = inspectList(properties.map((key) => [key, object[key]]), options, inspectProperty); + const symbolContents = inspectList(symbols.map((key) => [key, object[key]]), options, inspectProperty); + options.seen.pop(); + let sep = ""; + if (propertyContents && symbolContents) { + sep = ", "; + } + return `{ ${propertyContents}${sep}${symbolContents} }`; +} +__name(inspectObject, "inspectObject"); + +// node_modules/loupe/lib/class.js +var toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag ? Symbol.toStringTag : false; +function inspectClass(value, options) { + let name = ""; + if (toStringTag && toStringTag in value) { + name = value[toStringTag]; + } + name = name || value.constructor.name; + if (!name || name === "_class") { + name = ""; + } + options.truncate -= name.length; + return `${name}${inspectObject(value, options)}`; +} +__name(inspectClass, "inspectClass"); + +// node_modules/loupe/lib/arguments.js +function inspectArguments(args, options) { + if (args.length === 0) + return "Arguments[]"; + options.truncate -= 13; + return `Arguments[ ${inspectList(args, options)} ]`; +} +__name(inspectArguments, "inspectArguments"); + +// node_modules/loupe/lib/error.js +var errorKeys = [ + "stack", + "line", + "column", + "name", + "message", + "fileName", + "lineNumber", + "columnNumber", + "number", + "description", + "cause" +]; +function inspectObject2(error, options) { + const properties = Object.getOwnPropertyNames(error).filter((key) => errorKeys.indexOf(key) === -1); + const name = error.name; + options.truncate -= name.length; + let message = ""; + if (typeof error.message === "string") { + message = truncate(error.message, options.truncate); + } else { + properties.unshift("message"); + } + message = message ? `: ${message}` : ""; + options.truncate -= message.length + 5; + options.seen = options.seen || []; + if (options.seen.includes(error)) { + return "[Circular]"; + } + options.seen.push(error); + const propertyContents = inspectList(properties.map((key) => [key, error[key]]), options, inspectProperty); + return `${name}${message}${propertyContents ? ` { ${propertyContents} }` : ""}`; +} +__name(inspectObject2, "inspectObject"); + +// node_modules/loupe/lib/html.js +function inspectAttribute([key, value], options) { + options.truncate -= 3; + if (!value) { + return `${options.stylize(String(key), "yellow")}`; + } + return `${options.stylize(String(key), "yellow")}=${options.stylize(`"${value}"`, "string")}`; +} +__name(inspectAttribute, "inspectAttribute"); +function inspectNodeCollection(collection, options) { + return inspectList(collection, options, inspectNode, "\n"); +} +__name(inspectNodeCollection, "inspectNodeCollection"); +function inspectNode(node, options) { + switch (node.nodeType) { + case 1: + return inspectHTML(node, options); + case 3: + return options.inspect(node.data, options); + default: + return options.inspect(node, options); + } +} +__name(inspectNode, "inspectNode"); +function inspectHTML(element, options) { + const properties = element.getAttributeNames(); + const name = element.tagName.toLowerCase(); + const head = options.stylize(`<${name}`, "special"); + const headClose = options.stylize(`>`, "special"); + const tail = options.stylize(``, "special"); + options.truncate -= name.length * 2 + 5; + let propertyContents = ""; + if (properties.length > 0) { + propertyContents += " "; + propertyContents += inspectList(properties.map((key) => [key, element.getAttribute(key)]), options, inspectAttribute, " "); + } + options.truncate -= propertyContents.length; + const truncate2 = options.truncate; + let children = inspectNodeCollection(element.children, options); + if (children && children.length > truncate2) { + children = `${truncator}(${element.children.length})`; + } + return `${head}${propertyContents}${headClose}${children}${tail}`; +} +__name(inspectHTML, "inspectHTML"); + +// node_modules/loupe/lib/index.js +var symbolsSupported = typeof Symbol === "function" && typeof Symbol.for === "function"; +var chaiInspect = symbolsSupported ? Symbol.for("chai/inspect") : "@@chai/inspect"; +var nodeInspect = Symbol.for("nodejs.util.inspect.custom"); +var constructorMap = /* @__PURE__ */ new WeakMap(); +var stringTagMap = {}; +var baseTypesMap = { + undefined: /* @__PURE__ */ __name((value, options) => options.stylize("undefined", "undefined"), "undefined"), + null: /* @__PURE__ */ __name((value, options) => options.stylize("null", "null"), "null"), + boolean: /* @__PURE__ */ __name((value, options) => options.stylize(String(value), "boolean"), "boolean"), + Boolean: /* @__PURE__ */ __name((value, options) => options.stylize(String(value), "boolean"), "Boolean"), + number: inspectNumber, + Number: inspectNumber, + bigint: inspectBigInt, + BigInt: inspectBigInt, + string: inspectString, + String: inspectString, + function: inspectFunction, + Function: inspectFunction, + symbol: inspectSymbol, + // A Symbol polyfill will return `Symbol` not `symbol` from typedetect + Symbol: inspectSymbol, + Array: inspectArray, + Date: inspectDate, + Map: inspectMap, + Set: inspectSet, + RegExp: inspectRegExp, + Promise: promise_default, + // WeakSet, WeakMap are totally opaque to us + WeakSet: /* @__PURE__ */ __name((value, options) => options.stylize("WeakSet{\u2026}", "special"), "WeakSet"), + WeakMap: /* @__PURE__ */ __name((value, options) => options.stylize("WeakMap{\u2026}", "special"), "WeakMap"), + Arguments: inspectArguments, + Int8Array: inspectTypedArray, + Uint8Array: inspectTypedArray, + Uint8ClampedArray: inspectTypedArray, + Int16Array: inspectTypedArray, + Uint16Array: inspectTypedArray, + Int32Array: inspectTypedArray, + Uint32Array: inspectTypedArray, + Float32Array: inspectTypedArray, + Float64Array: inspectTypedArray, + Generator: /* @__PURE__ */ __name(() => "", "Generator"), + DataView: /* @__PURE__ */ __name(() => "", "DataView"), + ArrayBuffer: /* @__PURE__ */ __name(() => "", "ArrayBuffer"), + Error: inspectObject2, + HTMLCollection: inspectNodeCollection, + NodeList: inspectNodeCollection +}; +var inspectCustom = /* @__PURE__ */ __name((value, options, type3) => { + if (chaiInspect in value && typeof value[chaiInspect] === "function") { + return value[chaiInspect](options); + } + if (nodeInspect in value && typeof value[nodeInspect] === "function") { + return value[nodeInspect](options.depth, options); + } + if ("inspect" in value && typeof value.inspect === "function") { + return value.inspect(options.depth, options); + } + if ("constructor" in value && constructorMap.has(value.constructor)) { + return constructorMap.get(value.constructor)(value, options); + } + if (stringTagMap[type3]) { + return stringTagMap[type3](value, options); + } + return ""; +}, "inspectCustom"); +var toString = Object.prototype.toString; +function inspect(value, opts = {}) { + const options = normaliseOptions(opts, inspect); + const { customInspect } = options; + let type3 = value === null ? "null" : typeof value; + if (type3 === "object") { + type3 = toString.call(value).slice(8, -1); + } + if (type3 in baseTypesMap) { + return baseTypesMap[type3](value, options); + } + if (customInspect && value) { + const output = inspectCustom(value, options, type3); + if (output) { + if (typeof output === "string") + return output; + return inspect(output, options); + } + } + const proto = value ? Object.getPrototypeOf(value) : false; + if (proto === Object.prototype || proto === null) { + return inspectObject(value, options); + } + if (value && typeof HTMLElement === "function" && value instanceof HTMLElement) { + return inspectHTML(value, options); + } + if ("constructor" in value) { + if (value.constructor !== Object) { + return inspectClass(value, options); + } + return inspectObject(value, options); + } + if (value === Object(value)) { + return inspectObject(value, options); + } + return options.stylize(String(value), type3); +} +__name(inspect, "inspect"); + +// lib/chai/config.js +var config = { + /** + * ### config.includeStack + * + * User configurable property, influences whether stack trace + * is included in Assertion error message. Default of false + * suppresses stack trace in the error message. + * + * chai.config.includeStack = true; // enable stack on error + * + * @param {boolean} + * @public + */ + includeStack: false, + /** + * ### config.showDiff + * + * User configurable property, influences whether or not + * the `showDiff` flag should be included in the thrown + * AssertionErrors. `false` will always be `false`; `true` + * will be true when the assertion has requested a diff + * be shown. + * + * @param {boolean} + * @public + */ + showDiff: true, + /** + * ### config.truncateThreshold + * + * User configurable property, sets length threshold for actual and + * expected values in assertion errors. If this threshold is exceeded, for + * example for large data structures, the value is replaced with something + * like `[ Array(3) ]` or `{ Object (prop1, prop2) }`. + * + * Set it to zero if you want to disable truncating altogether. + * + * This is especially userful when doing assertions on arrays: having this + * set to a reasonable large value makes the failure messages readily + * inspectable. + * + * chai.config.truncateThreshold = 0; // disable truncating + * + * @param {number} + * @public + */ + truncateThreshold: 40, + /** + * ### config.useProxy + * + * User configurable property, defines if chai will use a Proxy to throw + * an error when a non-existent property is read, which protects users + * from typos when using property-based assertions. + * + * Set it to false if you want to disable this feature. + * + * chai.config.useProxy = false; // disable use of Proxy + * + * This feature is automatically disabled regardless of this config value + * in environments that don't support proxies. + * + * @param {boolean} + * @public + */ + useProxy: true, + /** + * ### config.proxyExcludedKeys + * + * User configurable property, defines which properties should be ignored + * instead of throwing an error if they do not exist on the assertion. + * This is only applied if the environment Chai is running in supports proxies and + * if the `useProxy` configuration setting is enabled. + * By default, `then` and `inspect` will not throw an error if they do not exist on the + * assertion object because the `.inspect` property is read by `util.inspect` (for example, when + * using `console.log` on the assertion object) and `.then` is necessary for promise type-checking. + * + * // By default these keys will not throw an error if they do not exist on the assertion object + * chai.config.proxyExcludedKeys = ['then', 'inspect']; + * + * @param {Array} + * @public + */ + proxyExcludedKeys: ["then", "catch", "inspect", "toJSON"], + /** + * ### config.deepEqual + * + * User configurable property, defines which a custom function to use for deepEqual + * comparisons. + * By default, the function used is the one from the `deep-eql` package without custom comparator. + * + * // use a custom comparator + * chai.config.deepEqual = (expected, actual) => { + * return chai.util.eql(expected, actual, { + * comparator: (expected, actual) => { + * // for non number comparison, use the default behavior + * if(typeof expected !== 'number') return null; + * // allow a difference of 10 between compared numbers + * return typeof actual === 'number' && Math.abs(actual - expected) < 10 + * } + * }) + * }; + * + * @param {Function} + * @public + */ + deepEqual: null +}; + +// lib/chai/utils/inspect.js +function inspect2(obj, showHidden, depth, colors) { + let options = { + colors, + depth: typeof depth === "undefined" ? 2 : depth, + showHidden, + truncate: config.truncateThreshold ? config.truncateThreshold : Infinity + }; + return inspect(obj, options); +} +__name(inspect2, "inspect"); + +// lib/chai/utils/objDisplay.js +function objDisplay(obj) { + let str = inspect2(obj), type3 = Object.prototype.toString.call(obj); + if (config.truncateThreshold && str.length >= config.truncateThreshold) { + if (type3 === "[object Function]") { + return !obj.name || obj.name === "" ? "[Function]" : "[Function: " + obj.name + "]"; + } else if (type3 === "[object Array]") { + return "[ Array(" + obj.length + ") ]"; + } else if (type3 === "[object Object]") { + let keys = Object.keys(obj), kstr = keys.length > 2 ? keys.splice(0, 2).join(", ") + ", ..." : keys.join(", "); + return "{ Object (" + kstr + ") }"; + } else { + return str; + } + } else { + return str; + } +} +__name(objDisplay, "objDisplay"); + +// lib/chai/utils/getMessage.js +function getMessage2(obj, args) { + let negate = flag(obj, "negate"); + let val = flag(obj, "object"); + let expected = args[3]; + let actual = getActual(obj, args); + let msg = negate ? args[2] : args[1]; + let flagMsg = flag(obj, "message"); + if (typeof msg === "function") msg = msg(); + msg = msg || ""; + msg = msg.replace(/#\{this\}/g, function() { + return objDisplay(val); + }).replace(/#\{act\}/g, function() { + return objDisplay(actual); + }).replace(/#\{exp\}/g, function() { + return objDisplay(expected); + }); + return flagMsg ? flagMsg + ": " + msg : msg; +} +__name(getMessage2, "getMessage"); + +// lib/chai/utils/transferFlags.js +function transferFlags(assertion, object, includeAll) { + let flags = assertion.__flags || (assertion.__flags = /* @__PURE__ */ Object.create(null)); + if (!object.__flags) { + object.__flags = /* @__PURE__ */ Object.create(null); + } + includeAll = arguments.length === 3 ? includeAll : true; + for (let flag3 in flags) { + if (includeAll || flag3 !== "object" && flag3 !== "ssfi" && flag3 !== "lockSsfi" && flag3 != "message") { + object.__flags[flag3] = flags[flag3]; + } + } +} +__name(transferFlags, "transferFlags"); + +// node_modules/deep-eql/index.js +function type2(obj) { + if (typeof obj === "undefined") { + return "undefined"; + } + if (obj === null) { + return "null"; + } + const stringTag = obj[Symbol.toStringTag]; + if (typeof stringTag === "string") { + return stringTag; + } + const sliceStart = 8; + const sliceEnd = -1; + return Object.prototype.toString.call(obj).slice(sliceStart, sliceEnd); +} +__name(type2, "type"); +function FakeMap() { + this._key = "chai/deep-eql__" + Math.random() + Date.now(); +} +__name(FakeMap, "FakeMap"); +FakeMap.prototype = { + get: /* @__PURE__ */ __name(function get(key) { + return key[this._key]; + }, "get"), + set: /* @__PURE__ */ __name(function set(key, value) { + if (Object.isExtensible(key)) { + Object.defineProperty(key, this._key, { + value, + configurable: true + }); + } + }, "set") +}; +var MemoizeMap = typeof WeakMap === "function" ? WeakMap : FakeMap; +function memoizeCompare(leftHandOperand, rightHandOperand, memoizeMap) { + if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) { + return null; + } + var leftHandMap = memoizeMap.get(leftHandOperand); + if (leftHandMap) { + var result = leftHandMap.get(rightHandOperand); + if (typeof result === "boolean") { + return result; + } + } + return null; +} +__name(memoizeCompare, "memoizeCompare"); +function memoizeSet(leftHandOperand, rightHandOperand, memoizeMap, result) { + if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) { + return; + } + var leftHandMap = memoizeMap.get(leftHandOperand); + if (leftHandMap) { + leftHandMap.set(rightHandOperand, result); + } else { + leftHandMap = new MemoizeMap(); + leftHandMap.set(rightHandOperand, result); + memoizeMap.set(leftHandOperand, leftHandMap); + } +} +__name(memoizeSet, "memoizeSet"); +var deep_eql_default = deepEqual; +function deepEqual(leftHandOperand, rightHandOperand, options) { + if (options && options.comparator) { + return extensiveDeepEqual(leftHandOperand, rightHandOperand, options); + } + var simpleResult = simpleEqual(leftHandOperand, rightHandOperand); + if (simpleResult !== null) { + return simpleResult; + } + return extensiveDeepEqual(leftHandOperand, rightHandOperand, options); +} +__name(deepEqual, "deepEqual"); +function simpleEqual(leftHandOperand, rightHandOperand) { + if (leftHandOperand === rightHandOperand) { + return leftHandOperand !== 0 || 1 / leftHandOperand === 1 / rightHandOperand; + } + if (leftHandOperand !== leftHandOperand && // eslint-disable-line no-self-compare + rightHandOperand !== rightHandOperand) { + return true; + } + if (isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) { + return false; + } + return null; +} +__name(simpleEqual, "simpleEqual"); +function extensiveDeepEqual(leftHandOperand, rightHandOperand, options) { + options = options || {}; + options.memoize = options.memoize === false ? false : options.memoize || new MemoizeMap(); + var comparator = options && options.comparator; + var memoizeResultLeft = memoizeCompare(leftHandOperand, rightHandOperand, options.memoize); + if (memoizeResultLeft !== null) { + return memoizeResultLeft; + } + var memoizeResultRight = memoizeCompare(rightHandOperand, leftHandOperand, options.memoize); + if (memoizeResultRight !== null) { + return memoizeResultRight; + } + if (comparator) { + var comparatorResult = comparator(leftHandOperand, rightHandOperand); + if (comparatorResult === false || comparatorResult === true) { + memoizeSet(leftHandOperand, rightHandOperand, options.memoize, comparatorResult); + return comparatorResult; + } + var simpleResult = simpleEqual(leftHandOperand, rightHandOperand); + if (simpleResult !== null) { + return simpleResult; + } + } + var leftHandType = type2(leftHandOperand); + if (leftHandType !== type2(rightHandOperand)) { + memoizeSet(leftHandOperand, rightHandOperand, options.memoize, false); + return false; + } + memoizeSet(leftHandOperand, rightHandOperand, options.memoize, true); + var result = extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandType, options); + memoizeSet(leftHandOperand, rightHandOperand, options.memoize, result); + return result; +} +__name(extensiveDeepEqual, "extensiveDeepEqual"); +function extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandType, options) { + switch (leftHandType) { + case "String": + case "Number": + case "Boolean": + case "Date": + return deepEqual(leftHandOperand.valueOf(), rightHandOperand.valueOf()); + case "Promise": + case "Symbol": + case "function": + case "WeakMap": + case "WeakSet": + return leftHandOperand === rightHandOperand; + case "Error": + return keysEqual(leftHandOperand, rightHandOperand, ["name", "message", "code"], options); + case "Arguments": + case "Int8Array": + case "Uint8Array": + case "Uint8ClampedArray": + case "Int16Array": + case "Uint16Array": + case "Int32Array": + case "Uint32Array": + case "Float32Array": + case "Float64Array": + case "Array": + return iterableEqual(leftHandOperand, rightHandOperand, options); + case "RegExp": + return regexpEqual(leftHandOperand, rightHandOperand); + case "Generator": + return generatorEqual(leftHandOperand, rightHandOperand, options); + case "DataView": + return iterableEqual(new Uint8Array(leftHandOperand.buffer), new Uint8Array(rightHandOperand.buffer), options); + case "ArrayBuffer": + return iterableEqual(new Uint8Array(leftHandOperand), new Uint8Array(rightHandOperand), options); + case "Set": + return entriesEqual(leftHandOperand, rightHandOperand, options); + case "Map": + return entriesEqual(leftHandOperand, rightHandOperand, options); + case "Temporal.PlainDate": + case "Temporal.PlainTime": + case "Temporal.PlainDateTime": + case "Temporal.Instant": + case "Temporal.ZonedDateTime": + case "Temporal.PlainYearMonth": + case "Temporal.PlainMonthDay": + return leftHandOperand.equals(rightHandOperand); + case "Temporal.Duration": + return leftHandOperand.total("nanoseconds") === rightHandOperand.total("nanoseconds"); + case "Temporal.TimeZone": + case "Temporal.Calendar": + return leftHandOperand.toString() === rightHandOperand.toString(); + default: + return objectEqual(leftHandOperand, rightHandOperand, options); + } +} +__name(extensiveDeepEqualByType, "extensiveDeepEqualByType"); +function regexpEqual(leftHandOperand, rightHandOperand) { + return leftHandOperand.toString() === rightHandOperand.toString(); +} +__name(regexpEqual, "regexpEqual"); +function entriesEqual(leftHandOperand, rightHandOperand, options) { + try { + if (leftHandOperand.size !== rightHandOperand.size) { + return false; + } + if (leftHandOperand.size === 0) { + return true; + } + } catch (sizeError) { + return false; + } + var leftHandItems = []; + var rightHandItems = []; + leftHandOperand.forEach(/* @__PURE__ */ __name(function gatherEntries(key, value) { + leftHandItems.push([key, value]); + }, "gatherEntries")); + rightHandOperand.forEach(/* @__PURE__ */ __name(function gatherEntries(key, value) { + rightHandItems.push([key, value]); + }, "gatherEntries")); + return iterableEqual(leftHandItems.sort(), rightHandItems.sort(), options); +} +__name(entriesEqual, "entriesEqual"); +function iterableEqual(leftHandOperand, rightHandOperand, options) { + var length = leftHandOperand.length; + if (length !== rightHandOperand.length) { + return false; + } + if (length === 0) { + return true; + } + var index = -1; + while (++index < length) { + if (deepEqual(leftHandOperand[index], rightHandOperand[index], options) === false) { + return false; + } + } + return true; +} +__name(iterableEqual, "iterableEqual"); +function generatorEqual(leftHandOperand, rightHandOperand, options) { + return iterableEqual(getGeneratorEntries(leftHandOperand), getGeneratorEntries(rightHandOperand), options); +} +__name(generatorEqual, "generatorEqual"); +function hasIteratorFunction(target) { + return typeof Symbol !== "undefined" && typeof target === "object" && typeof Symbol.iterator !== "undefined" && typeof target[Symbol.iterator] === "function"; +} +__name(hasIteratorFunction, "hasIteratorFunction"); +function getIteratorEntries(target) { + if (hasIteratorFunction(target)) { + try { + return getGeneratorEntries(target[Symbol.iterator]()); + } catch (iteratorError) { + return []; + } + } + return []; +} +__name(getIteratorEntries, "getIteratorEntries"); +function getGeneratorEntries(generator) { + var generatorResult = generator.next(); + var accumulator = [generatorResult.value]; + while (generatorResult.done === false) { + generatorResult = generator.next(); + accumulator.push(generatorResult.value); + } + return accumulator; +} +__name(getGeneratorEntries, "getGeneratorEntries"); +function getEnumerableKeys(target) { + var keys = []; + for (var key in target) { + keys.push(key); + } + return keys; +} +__name(getEnumerableKeys, "getEnumerableKeys"); +function getEnumerableSymbols(target) { + var keys = []; + var allKeys = Object.getOwnPropertySymbols(target); + for (var i = 0; i < allKeys.length; i += 1) { + var key = allKeys[i]; + if (Object.getOwnPropertyDescriptor(target, key).enumerable) { + keys.push(key); + } + } + return keys; +} +__name(getEnumerableSymbols, "getEnumerableSymbols"); +function keysEqual(leftHandOperand, rightHandOperand, keys, options) { + var length = keys.length; + if (length === 0) { + return true; + } + for (var i = 0; i < length; i += 1) { + if (deepEqual(leftHandOperand[keys[i]], rightHandOperand[keys[i]], options) === false) { + return false; + } + } + return true; +} +__name(keysEqual, "keysEqual"); +function objectEqual(leftHandOperand, rightHandOperand, options) { + var leftHandKeys = getEnumerableKeys(leftHandOperand); + var rightHandKeys = getEnumerableKeys(rightHandOperand); + var leftHandSymbols = getEnumerableSymbols(leftHandOperand); + var rightHandSymbols = getEnumerableSymbols(rightHandOperand); + leftHandKeys = leftHandKeys.concat(leftHandSymbols); + rightHandKeys = rightHandKeys.concat(rightHandSymbols); + if (leftHandKeys.length && leftHandKeys.length === rightHandKeys.length) { + if (iterableEqual(mapSymbols(leftHandKeys).sort(), mapSymbols(rightHandKeys).sort()) === false) { + return false; + } + return keysEqual(leftHandOperand, rightHandOperand, leftHandKeys, options); + } + var leftHandEntries = getIteratorEntries(leftHandOperand); + var rightHandEntries = getIteratorEntries(rightHandOperand); + if (leftHandEntries.length && leftHandEntries.length === rightHandEntries.length) { + leftHandEntries.sort(); + rightHandEntries.sort(); + return iterableEqual(leftHandEntries, rightHandEntries, options); + } + if (leftHandKeys.length === 0 && leftHandEntries.length === 0 && rightHandKeys.length === 0 && rightHandEntries.length === 0) { + return true; + } + return false; +} +__name(objectEqual, "objectEqual"); +function isPrimitive(value) { + return value === null || typeof value !== "object"; +} +__name(isPrimitive, "isPrimitive"); +function mapSymbols(arr) { + return arr.map(/* @__PURE__ */ __name(function mapSymbol(entry) { + if (typeof entry === "symbol") { + return entry.toString(); + } + return entry; + }, "mapSymbol")); +} +__name(mapSymbols, "mapSymbols"); + +// node_modules/pathval/index.js +function hasProperty(obj, name) { + if (typeof obj === "undefined" || obj === null) { + return false; + } + return name in Object(obj); +} +__name(hasProperty, "hasProperty"); +function parsePath(path) { + const str = path.replace(/([^\\])\[/g, "$1.["); + const parts = str.match(/(\\\.|[^.]+?)+/g); + return parts.map((value) => { + if (value === "constructor" || value === "__proto__" || value === "prototype") { + return {}; + } + const regexp = /^\[(\d+)\]$/; + const mArr = regexp.exec(value); + let parsed = null; + if (mArr) { + parsed = { i: parseFloat(mArr[1]) }; + } else { + parsed = { p: value.replace(/\\([.[\]])/g, "$1") }; + } + return parsed; + }); +} +__name(parsePath, "parsePath"); +function internalGetPathValue(obj, parsed, pathDepth) { + let temporaryValue = obj; + let res = null; + pathDepth = typeof pathDepth === "undefined" ? parsed.length : pathDepth; + for (let i = 0; i < pathDepth; i++) { + const part = parsed[i]; + if (temporaryValue) { + if (typeof part.p === "undefined") { + temporaryValue = temporaryValue[part.i]; + } else { + temporaryValue = temporaryValue[part.p]; + } + if (i === pathDepth - 1) { + res = temporaryValue; + } + } + } + return res; +} +__name(internalGetPathValue, "internalGetPathValue"); +function getPathInfo(obj, path) { + const parsed = parsePath(path); + const last = parsed[parsed.length - 1]; + const info = { + parent: parsed.length > 1 ? internalGetPathValue(obj, parsed, parsed.length - 1) : obj, + name: last.p || last.i, + value: internalGetPathValue(obj, parsed) + }; + info.exists = hasProperty(info.parent, info.name); + return info; +} +__name(getPathInfo, "getPathInfo"); + +// lib/chai/assertion.js +var Assertion = class _Assertion { + static { + __name(this, "Assertion"); + } + /** @type {{}} */ + __flags = {}; + /** + * Creates object for chaining. + * `Assertion` objects contain metadata in the form of flags. Three flags can + * be assigned during instantiation by passing arguments to this constructor: + * + * - `object`: This flag contains the target of the assertion. For example, in + * the assertion `expect(numKittens).to.equal(7);`, the `object` flag will + * contain `numKittens` so that the `equal` assertion can reference it when + * needed. + * + * - `message`: This flag contains an optional custom error message to be + * prepended to the error message that's generated by the assertion when it + * fails. + * + * - `ssfi`: This flag stands for "start stack function indicator". It + * contains a function reference that serves as the starting point for + * removing frames from the stack trace of the error that's created by the + * assertion when it fails. The goal is to provide a cleaner stack trace to + * end users by removing Chai's internal functions. Note that it only works + * in environments that support `Error.captureStackTrace`, and only when + * `Chai.config.includeStack` hasn't been set to `false`. + * + * - `lockSsfi`: This flag controls whether or not the given `ssfi` flag + * should retain its current value, even as assertions are chained off of + * this object. This is usually set to `true` when creating a new assertion + * from within another assertion. It's also temporarily set to `true` before + * an overwritten assertion gets called by the overwriting assertion. + * + * - `eql`: This flag contains the deepEqual function to be used by the assertion. + * + * @param {unknown} obj target of the assertion + * @param {string} [msg] (optional) custom error message + * @param {Function} [ssfi] (optional) starting point for removing stack frames + * @param {boolean} [lockSsfi] (optional) whether or not the ssfi flag is locked + */ + constructor(obj, msg, ssfi, lockSsfi) { + flag(this, "ssfi", ssfi || _Assertion); + flag(this, "lockSsfi", lockSsfi); + flag(this, "object", obj); + flag(this, "message", msg); + flag(this, "eql", config.deepEqual || deep_eql_default); + return proxify(this); + } + /** @returns {boolean} */ + static get includeStack() { + console.warn( + "Assertion.includeStack is deprecated, use chai.config.includeStack instead." + ); + return config.includeStack; + } + /** @param {boolean} value */ + static set includeStack(value) { + console.warn( + "Assertion.includeStack is deprecated, use chai.config.includeStack instead." + ); + config.includeStack = value; + } + /** @returns {boolean} */ + static get showDiff() { + console.warn( + "Assertion.showDiff is deprecated, use chai.config.showDiff instead." + ); + return config.showDiff; + } + /** @param {boolean} value */ + static set showDiff(value) { + console.warn( + "Assertion.showDiff is deprecated, use chai.config.showDiff instead." + ); + config.showDiff = value; + } + /** + * @param {string} name + * @param {Function} fn + */ + static addProperty(name, fn) { + addProperty(this.prototype, name, fn); + } + /** + * @param {string} name + * @param {Function} fn + */ + static addMethod(name, fn) { + addMethod(this.prototype, name, fn); + } + /** + * @param {string} name + * @param {Function} fn + * @param {Function} chainingBehavior + */ + static addChainableMethod(name, fn, chainingBehavior) { + addChainableMethod(this.prototype, name, fn, chainingBehavior); + } + /** + * @param {string} name + * @param {Function} fn + */ + static overwriteProperty(name, fn) { + overwriteProperty(this.prototype, name, fn); + } + /** + * @param {string} name + * @param {Function} fn + */ + static overwriteMethod(name, fn) { + overwriteMethod(this.prototype, name, fn); + } + /** + * @param {string} name + * @param {Function} fn + * @param {Function} chainingBehavior + */ + static overwriteChainableMethod(name, fn, chainingBehavior) { + overwriteChainableMethod(this.prototype, name, fn, chainingBehavior); + } + /** + * ### .assert(expression, message, negateMessage, expected, actual, showDiff) + * + * Executes an expression and check expectations. Throws AssertionError for reporting if test doesn't pass. + * + * @name assert + * @param {unknown} _expr to be tested + * @param {string | Function} msg or function that returns message to display if expression fails + * @param {string | Function} _negateMsg or function that returns negatedMessage to display if negated expression fails + * @param {unknown} expected value (remember to check for negation) + * @param {unknown} _actual (optional) will default to `this.obj` + * @param {boolean} showDiff (optional) when set to `true`, assert will display a diff in addition to the message if expression fails + * @returns {void} + */ + assert(_expr, msg, _negateMsg, expected, _actual, showDiff) { + const ok = test(this, arguments); + if (false !== showDiff) showDiff = true; + if (void 0 === expected && void 0 === _actual) showDiff = false; + if (true !== config.showDiff) showDiff = false; + if (!ok) { + msg = getMessage2(this, arguments); + const actual = getActual(this, arguments); + const assertionErrorObjectProperties = { + actual, + expected, + showDiff + }; + const operator = getOperator(this, arguments); + if (operator) { + assertionErrorObjectProperties.operator = operator; + } + throw new AssertionError( + msg, + assertionErrorObjectProperties, + // @ts-expect-error Not sure what to do about these types yet + config.includeStack ? this.assert : flag(this, "ssfi") + ); + } + } + /** + * Quick reference to stored `actual` value for plugin developers. + * + * @returns {unknown} + */ + get _obj() { + return flag(this, "object"); + } + /** + * Quick reference to stored `actual` value for plugin developers. + * + * @param {unknown} val + */ + set _obj(val) { + flag(this, "object", val); + } +}; + +// lib/chai/utils/isProxyEnabled.js +function isProxyEnabled() { + return config.useProxy && typeof Proxy !== "undefined" && typeof Reflect !== "undefined"; +} +__name(isProxyEnabled, "isProxyEnabled"); + +// lib/chai/utils/addProperty.js +function addProperty(ctx, name, getter) { + getter = getter === void 0 ? function() { + } : getter; + Object.defineProperty(ctx, name, { + get: /* @__PURE__ */ __name(function propertyGetter() { + if (!isProxyEnabled() && !flag(this, "lockSsfi")) { + flag(this, "ssfi", propertyGetter); + } + let result = getter.call(this); + if (result !== void 0) return result; + let newAssertion = new Assertion(); + transferFlags(this, newAssertion); + return newAssertion; + }, "propertyGetter"), + configurable: true + }); +} +__name(addProperty, "addProperty"); + +// lib/chai/utils/addLengthGuard.js +var fnLengthDesc = Object.getOwnPropertyDescriptor(function() { +}, "length"); +function addLengthGuard(fn, assertionName, isChainable) { + if (!fnLengthDesc.configurable) return fn; + Object.defineProperty(fn, "length", { + get: /* @__PURE__ */ __name(function() { + if (isChainable) { + throw Error( + "Invalid Chai property: " + assertionName + '.length. Due to a compatibility issue, "length" cannot directly follow "' + assertionName + '". Use "' + assertionName + '.lengthOf" instead.' + ); + } + throw Error( + "Invalid Chai property: " + assertionName + '.length. See docs for proper usage of "' + assertionName + '".' + ); + }, "get") + }); + return fn; +} +__name(addLengthGuard, "addLengthGuard"); + +// lib/chai/utils/getProperties.js +function getProperties(object) { + let result = Object.getOwnPropertyNames(object); + function addProperty2(property) { + if (result.indexOf(property) === -1) { + result.push(property); + } + } + __name(addProperty2, "addProperty"); + let proto = Object.getPrototypeOf(object); + while (proto !== null) { + Object.getOwnPropertyNames(proto).forEach(addProperty2); + proto = Object.getPrototypeOf(proto); + } + return result; +} +__name(getProperties, "getProperties"); + +// lib/chai/utils/proxify.js +var builtins = ["__flags", "__methods", "_obj", "assert"]; +function proxify(obj, nonChainableMethodName) { + if (!isProxyEnabled()) return obj; + return new Proxy(obj, { + get: /* @__PURE__ */ __name(function proxyGetter(target, property) { + if (typeof property === "string" && config.proxyExcludedKeys.indexOf(property) === -1 && !Reflect.has(target, property)) { + if (nonChainableMethodName) { + throw Error( + "Invalid Chai property: " + nonChainableMethodName + "." + property + '. See docs for proper usage of "' + nonChainableMethodName + '".' + ); + } + let suggestion = null; + let suggestionDistance = 4; + getProperties(target).forEach(function(prop) { + if ( + // we actually mean to check `Object.prototype` here + // eslint-disable-next-line no-prototype-builtins + !Object.prototype.hasOwnProperty(prop) && builtins.indexOf(prop) === -1 + ) { + let dist = stringDistanceCapped(property, prop, suggestionDistance); + if (dist < suggestionDistance) { + suggestion = prop; + suggestionDistance = dist; + } + } + }); + if (suggestion !== null) { + throw Error( + "Invalid Chai property: " + property + '. Did you mean "' + suggestion + '"?' + ); + } else { + throw Error("Invalid Chai property: " + property); + } + } + if (builtins.indexOf(property) === -1 && !flag(target, "lockSsfi")) { + flag(target, "ssfi", proxyGetter); + } + return Reflect.get(target, property); + }, "proxyGetter") + }); +} +__name(proxify, "proxify"); +function stringDistanceCapped(strA, strB, cap) { + if (Math.abs(strA.length - strB.length) >= cap) { + return cap; + } + let memo = []; + for (let i = 0; i <= strA.length; i++) { + memo[i] = Array(strB.length + 1).fill(0); + memo[i][0] = i; + } + for (let j = 0; j < strB.length; j++) { + memo[0][j] = j; + } + for (let i = 1; i <= strA.length; i++) { + let ch = strA.charCodeAt(i - 1); + for (let j = 1; j <= strB.length; j++) { + if (Math.abs(i - j) >= cap) { + memo[i][j] = cap; + continue; + } + memo[i][j] = Math.min( + memo[i - 1][j] + 1, + memo[i][j - 1] + 1, + memo[i - 1][j - 1] + (ch === strB.charCodeAt(j - 1) ? 0 : 1) + ); + } + } + return memo[strA.length][strB.length]; +} +__name(stringDistanceCapped, "stringDistanceCapped"); + +// lib/chai/utils/addMethod.js +function addMethod(ctx, name, method) { + let methodWrapper = /* @__PURE__ */ __name(function() { + if (!flag(this, "lockSsfi")) { + flag(this, "ssfi", methodWrapper); + } + let result = method.apply(this, arguments); + if (result !== void 0) return result; + let newAssertion = new Assertion(); + transferFlags(this, newAssertion); + return newAssertion; + }, "methodWrapper"); + addLengthGuard(methodWrapper, name, false); + ctx[name] = proxify(methodWrapper, name); +} +__name(addMethod, "addMethod"); + +// lib/chai/utils/overwriteProperty.js +function overwriteProperty(ctx, name, getter) { + let _get = Object.getOwnPropertyDescriptor(ctx, name), _super = /* @__PURE__ */ __name(function() { + }, "_super"); + if (_get && "function" === typeof _get.get) _super = _get.get; + Object.defineProperty(ctx, name, { + get: /* @__PURE__ */ __name(function overwritingPropertyGetter() { + if (!isProxyEnabled() && !flag(this, "lockSsfi")) { + flag(this, "ssfi", overwritingPropertyGetter); + } + let origLockSsfi = flag(this, "lockSsfi"); + flag(this, "lockSsfi", true); + let result = getter(_super).call(this); + flag(this, "lockSsfi", origLockSsfi); + if (result !== void 0) { + return result; + } + let newAssertion = new Assertion(); + transferFlags(this, newAssertion); + return newAssertion; + }, "overwritingPropertyGetter"), + configurable: true + }); +} +__name(overwriteProperty, "overwriteProperty"); + +// lib/chai/utils/overwriteMethod.js +function overwriteMethod(ctx, name, method) { + let _method = ctx[name], _super = /* @__PURE__ */ __name(function() { + throw new Error(name + " is not a function"); + }, "_super"); + if (_method && "function" === typeof _method) _super = _method; + let overwritingMethodWrapper = /* @__PURE__ */ __name(function() { + if (!flag(this, "lockSsfi")) { + flag(this, "ssfi", overwritingMethodWrapper); + } + let origLockSsfi = flag(this, "lockSsfi"); + flag(this, "lockSsfi", true); + let result = method(_super).apply(this, arguments); + flag(this, "lockSsfi", origLockSsfi); + if (result !== void 0) { + return result; + } + let newAssertion = new Assertion(); + transferFlags(this, newAssertion); + return newAssertion; + }, "overwritingMethodWrapper"); + addLengthGuard(overwritingMethodWrapper, name, false); + ctx[name] = proxify(overwritingMethodWrapper, name); +} +__name(overwriteMethod, "overwriteMethod"); + +// lib/chai/utils/addChainableMethod.js +var canSetPrototype = typeof Object.setPrototypeOf === "function"; +var testFn = /* @__PURE__ */ __name(function() { +}, "testFn"); +var excludeNames = Object.getOwnPropertyNames(testFn).filter(function(name) { + let propDesc = Object.getOwnPropertyDescriptor(testFn, name); + if (typeof propDesc !== "object") return true; + return !propDesc.configurable; +}); +var call = Function.prototype.call; +var apply = Function.prototype.apply; +function addChainableMethod(ctx, name, method, chainingBehavior) { + if (typeof chainingBehavior !== "function") { + chainingBehavior = /* @__PURE__ */ __name(function() { + }, "chainingBehavior"); + } + let chainableBehavior = { + method, + chainingBehavior + }; + if (!ctx.__methods) { + ctx.__methods = {}; + } + ctx.__methods[name] = chainableBehavior; + Object.defineProperty(ctx, name, { + get: /* @__PURE__ */ __name(function chainableMethodGetter() { + chainableBehavior.chainingBehavior.call(this); + let chainableMethodWrapper = /* @__PURE__ */ __name(function() { + if (!flag(this, "lockSsfi")) { + flag(this, "ssfi", chainableMethodWrapper); + } + let result = chainableBehavior.method.apply(this, arguments); + if (result !== void 0) { + return result; + } + let newAssertion = new Assertion(); + transferFlags(this, newAssertion); + return newAssertion; + }, "chainableMethodWrapper"); + addLengthGuard(chainableMethodWrapper, name, true); + if (canSetPrototype) { + let prototype = Object.create(this); + prototype.call = call; + prototype.apply = apply; + Object.setPrototypeOf(chainableMethodWrapper, prototype); + } else { + let asserterNames = Object.getOwnPropertyNames(ctx); + asserterNames.forEach(function(asserterName) { + if (excludeNames.indexOf(asserterName) !== -1) { + return; + } + let pd = Object.getOwnPropertyDescriptor(ctx, asserterName); + Object.defineProperty(chainableMethodWrapper, asserterName, pd); + }); + } + transferFlags(this, chainableMethodWrapper); + return proxify(chainableMethodWrapper); + }, "chainableMethodGetter"), + configurable: true + }); +} +__name(addChainableMethod, "addChainableMethod"); + +// lib/chai/utils/overwriteChainableMethod.js +function overwriteChainableMethod(ctx, name, method, chainingBehavior) { + let chainableBehavior = ctx.__methods[name]; + let _chainingBehavior = chainableBehavior.chainingBehavior; + chainableBehavior.chainingBehavior = /* @__PURE__ */ __name(function overwritingChainableMethodGetter() { + let result = chainingBehavior(_chainingBehavior).call(this); + if (result !== void 0) { + return result; + } + let newAssertion = new Assertion(); + transferFlags(this, newAssertion); + return newAssertion; + }, "overwritingChainableMethodGetter"); + let _method = chainableBehavior.method; + chainableBehavior.method = /* @__PURE__ */ __name(function overwritingChainableMethodWrapper() { + let result = method(_method).apply(this, arguments); + if (result !== void 0) { + return result; + } + let newAssertion = new Assertion(); + transferFlags(this, newAssertion); + return newAssertion; + }, "overwritingChainableMethodWrapper"); +} +__name(overwriteChainableMethod, "overwriteChainableMethod"); + +// lib/chai/utils/compareByInspect.js +function compareByInspect(a, b) { + return inspect2(a) < inspect2(b) ? -1 : 1; +} +__name(compareByInspect, "compareByInspect"); + +// lib/chai/utils/getOwnEnumerablePropertySymbols.js +function getOwnEnumerablePropertySymbols(obj) { + if (typeof Object.getOwnPropertySymbols !== "function") return []; + return Object.getOwnPropertySymbols(obj).filter(function(sym) { + return Object.getOwnPropertyDescriptor(obj, sym).enumerable; + }); +} +__name(getOwnEnumerablePropertySymbols, "getOwnEnumerablePropertySymbols"); + +// lib/chai/utils/getOwnEnumerableProperties.js +function getOwnEnumerableProperties(obj) { + return Object.keys(obj).concat(getOwnEnumerablePropertySymbols(obj)); +} +__name(getOwnEnumerableProperties, "getOwnEnumerableProperties"); + +// lib/chai/utils/isNaN.js +var isNaN2 = Number.isNaN; + +// lib/chai/utils/getOperator.js +function isObjectType(obj) { + let objectType = type(obj); + let objectTypes = ["Array", "Object", "Function"]; + return objectTypes.indexOf(objectType) !== -1; +} +__name(isObjectType, "isObjectType"); +function getOperator(obj, args) { + let operator = flag(obj, "operator"); + let negate = flag(obj, "negate"); + let expected = args[3]; + let msg = negate ? args[2] : args[1]; + if (operator) { + return operator; + } + if (typeof msg === "function") msg = msg(); + msg = msg || ""; + if (!msg) { + return void 0; + } + if (/\shave\s/.test(msg)) { + return void 0; + } + let isObject = isObjectType(expected); + if (/\snot\s/.test(msg)) { + return isObject ? "notDeepStrictEqual" : "notStrictEqual"; + } + return isObject ? "deepStrictEqual" : "strictEqual"; +} +__name(getOperator, "getOperator"); + +// lib/chai/utils/index.js +function getName(fn) { + return fn.name; +} +__name(getName, "getName"); +function isRegExp2(obj) { + return Object.prototype.toString.call(obj) === "[object RegExp]"; +} +__name(isRegExp2, "isRegExp"); +function isNumeric(obj) { + return ["Number", "BigInt"].includes(type(obj)); +} +__name(isNumeric, "isNumeric"); + +// lib/chai/core/assertions.js +var { flag: flag2 } = utils_exports; +[ + "to", + "be", + "been", + "is", + "and", + "has", + "have", + "with", + "that", + "which", + "at", + "of", + "same", + "but", + "does", + "still", + "also" +].forEach(function(chain) { + Assertion.addProperty(chain); +}); +Assertion.addProperty("not", function() { + flag2(this, "negate", true); +}); +Assertion.addProperty("deep", function() { + flag2(this, "deep", true); +}); +Assertion.addProperty("nested", function() { + flag2(this, "nested", true); +}); +Assertion.addProperty("own", function() { + flag2(this, "own", true); +}); +Assertion.addProperty("ordered", function() { + flag2(this, "ordered", true); +}); +Assertion.addProperty("any", function() { + flag2(this, "any", true); + flag2(this, "all", false); +}); +Assertion.addProperty("all", function() { + flag2(this, "all", true); + flag2(this, "any", false); +}); +var functionTypes = { + function: [ + "function", + "asyncfunction", + "generatorfunction", + "asyncgeneratorfunction" + ], + asyncfunction: ["asyncfunction", "asyncgeneratorfunction"], + generatorfunction: ["generatorfunction", "asyncgeneratorfunction"], + asyncgeneratorfunction: ["asyncgeneratorfunction"] +}; +function an(type3, msg) { + if (msg) flag2(this, "message", msg); + type3 = type3.toLowerCase(); + let obj = flag2(this, "object"), article = ~["a", "e", "i", "o", "u"].indexOf(type3.charAt(0)) ? "an " : "a "; + const detectedType = type(obj).toLowerCase(); + if (functionTypes["function"].includes(type3)) { + this.assert( + functionTypes[type3].includes(detectedType), + "expected #{this} to be " + article + type3, + "expected #{this} not to be " + article + type3 + ); + } else { + this.assert( + type3 === detectedType, + "expected #{this} to be " + article + type3, + "expected #{this} not to be " + article + type3 + ); + } +} +__name(an, "an"); +Assertion.addChainableMethod("an", an); +Assertion.addChainableMethod("a", an); +function SameValueZero(a, b) { + return isNaN2(a) && isNaN2(b) || a === b; +} +__name(SameValueZero, "SameValueZero"); +function includeChainingBehavior() { + flag2(this, "contains", true); +} +__name(includeChainingBehavior, "includeChainingBehavior"); +function include(val, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), objType = type(obj).toLowerCase(), flagMsg = flag2(this, "message"), negate = flag2(this, "negate"), ssfi = flag2(this, "ssfi"), isDeep = flag2(this, "deep"), descriptor = isDeep ? "deep " : "", isEql = isDeep ? flag2(this, "eql") : SameValueZero; + flagMsg = flagMsg ? flagMsg + ": " : ""; + let included = false; + switch (objType) { + case "string": + included = obj.indexOf(val) !== -1; + break; + case "weakset": + if (isDeep) { + throw new AssertionError( + flagMsg + "unable to use .deep.include with WeakSet", + void 0, + ssfi + ); + } + included = obj.has(val); + break; + case "map": + obj.forEach(function(item) { + included = included || isEql(item, val); + }); + break; + case "set": + if (isDeep) { + obj.forEach(function(item) { + included = included || isEql(item, val); + }); + } else { + included = obj.has(val); + } + break; + case "array": + if (isDeep) { + included = obj.some(function(item) { + return isEql(item, val); + }); + } else { + included = obj.indexOf(val) !== -1; + } + break; + default: { + if (val !== Object(val)) { + throw new AssertionError( + flagMsg + "the given combination of arguments (" + objType + " and " + type(val).toLowerCase() + ") is invalid for this assertion. You can use an array, a map, an object, a set, a string, or a weakset instead of a " + type(val).toLowerCase(), + void 0, + ssfi + ); + } + let props = Object.keys(val); + let firstErr = null; + let numErrs = 0; + props.forEach(function(prop) { + let propAssertion = new Assertion(obj); + transferFlags(this, propAssertion, true); + flag2(propAssertion, "lockSsfi", true); + if (!negate || props.length === 1) { + propAssertion.property(prop, val[prop]); + return; + } + try { + propAssertion.property(prop, val[prop]); + } catch (err) { + if (!check_error_exports.compatibleConstructor(err, AssertionError)) { + throw err; + } + if (firstErr === null) firstErr = err; + numErrs++; + } + }, this); + if (negate && props.length > 1 && numErrs === props.length) { + throw firstErr; + } + return; + } + } + this.assert( + included, + "expected #{this} to " + descriptor + "include " + inspect2(val), + "expected #{this} to not " + descriptor + "include " + inspect2(val) + ); +} +__name(include, "include"); +Assertion.addChainableMethod("include", include, includeChainingBehavior); +Assertion.addChainableMethod("contain", include, includeChainingBehavior); +Assertion.addChainableMethod("contains", include, includeChainingBehavior); +Assertion.addChainableMethod("includes", include, includeChainingBehavior); +Assertion.addProperty("ok", function() { + this.assert( + flag2(this, "object"), + "expected #{this} to be truthy", + "expected #{this} to be falsy" + ); +}); +Assertion.addProperty("true", function() { + this.assert( + true === flag2(this, "object"), + "expected #{this} to be true", + "expected #{this} to be false", + flag2(this, "negate") ? false : true + ); +}); +Assertion.addProperty("numeric", function() { + const object = flag2(this, "object"); + this.assert( + ["Number", "BigInt"].includes(type(object)), + "expected #{this} to be numeric", + "expected #{this} to not be numeric", + flag2(this, "negate") ? false : true + ); +}); +Assertion.addProperty("callable", function() { + const val = flag2(this, "object"); + const ssfi = flag2(this, "ssfi"); + const message = flag2(this, "message"); + const msg = message ? `${message}: ` : ""; + const negate = flag2(this, "negate"); + const assertionMessage = negate ? `${msg}expected ${inspect2(val)} not to be a callable function` : `${msg}expected ${inspect2(val)} to be a callable function`; + const isCallable = [ + "Function", + "AsyncFunction", + "GeneratorFunction", + "AsyncGeneratorFunction" + ].includes(type(val)); + if (isCallable && negate || !isCallable && !negate) { + throw new AssertionError(assertionMessage, void 0, ssfi); + } +}); +Assertion.addProperty("false", function() { + this.assert( + false === flag2(this, "object"), + "expected #{this} to be false", + "expected #{this} to be true", + flag2(this, "negate") ? true : false + ); +}); +Assertion.addProperty("null", function() { + this.assert( + null === flag2(this, "object"), + "expected #{this} to be null", + "expected #{this} not to be null" + ); +}); +Assertion.addProperty("undefined", function() { + this.assert( + void 0 === flag2(this, "object"), + "expected #{this} to be undefined", + "expected #{this} not to be undefined" + ); +}); +Assertion.addProperty("NaN", function() { + this.assert( + isNaN2(flag2(this, "object")), + "expected #{this} to be NaN", + "expected #{this} not to be NaN" + ); +}); +function assertExist() { + let val = flag2(this, "object"); + this.assert( + val !== null && val !== void 0, + "expected #{this} to exist", + "expected #{this} to not exist" + ); +} +__name(assertExist, "assertExist"); +Assertion.addProperty("exist", assertExist); +Assertion.addProperty("exists", assertExist); +Assertion.addProperty("empty", function() { + let val = flag2(this, "object"), ssfi = flag2(this, "ssfi"), flagMsg = flag2(this, "message"), itemsCount; + flagMsg = flagMsg ? flagMsg + ": " : ""; + switch (type(val).toLowerCase()) { + case "array": + case "string": + itemsCount = val.length; + break; + case "map": + case "set": + itemsCount = val.size; + break; + case "weakmap": + case "weakset": + throw new AssertionError( + flagMsg + ".empty was passed a weak collection", + void 0, + ssfi + ); + case "function": { + const msg = flagMsg + ".empty was passed a function " + getName(val); + throw new AssertionError(msg.trim(), void 0, ssfi); + } + default: + if (val !== Object(val)) { + throw new AssertionError( + flagMsg + ".empty was passed non-string primitive " + inspect2(val), + void 0, + ssfi + ); + } + itemsCount = Object.keys(val).length; + } + this.assert( + 0 === itemsCount, + "expected #{this} to be empty", + "expected #{this} not to be empty" + ); +}); +function checkArguments() { + let obj = flag2(this, "object"), type3 = type(obj); + this.assert( + "Arguments" === type3, + "expected #{this} to be arguments but got " + type3, + "expected #{this} to not be arguments" + ); +} +__name(checkArguments, "checkArguments"); +Assertion.addProperty("arguments", checkArguments); +Assertion.addProperty("Arguments", checkArguments); +function assertEqual(val, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"); + if (flag2(this, "deep")) { + let prevLockSsfi = flag2(this, "lockSsfi"); + flag2(this, "lockSsfi", true); + this.eql(val); + flag2(this, "lockSsfi", prevLockSsfi); + } else { + this.assert( + val === obj, + "expected #{this} to equal #{exp}", + "expected #{this} to not equal #{exp}", + val, + this._obj, + true + ); + } +} +__name(assertEqual, "assertEqual"); +Assertion.addMethod("equal", assertEqual); +Assertion.addMethod("equals", assertEqual); +Assertion.addMethod("eq", assertEqual); +function assertEql(obj, msg) { + if (msg) flag2(this, "message", msg); + let eql = flag2(this, "eql"); + this.assert( + eql(obj, flag2(this, "object")), + "expected #{this} to deeply equal #{exp}", + "expected #{this} to not deeply equal #{exp}", + obj, + this._obj, + true + ); +} +__name(assertEql, "assertEql"); +Assertion.addMethod("eql", assertEql); +Assertion.addMethod("eqls", assertEql); +function assertAbove(n, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), doLength = flag2(this, "doLength"), flagMsg = flag2(this, "message"), msgPrefix = flagMsg ? flagMsg + ": " : "", ssfi = flag2(this, "ssfi"), objType = type(obj).toLowerCase(), nType = type(n).toLowerCase(); + if (doLength && objType !== "map" && objType !== "set") { + new Assertion(obj, flagMsg, ssfi, true).to.have.property("length"); + } + if (!doLength && objType === "date" && nType !== "date") { + throw new AssertionError( + msgPrefix + "the argument to above must be a date", + void 0, + ssfi + ); + } else if (!isNumeric(n) && (doLength || isNumeric(obj))) { + throw new AssertionError( + msgPrefix + "the argument to above must be a number", + void 0, + ssfi + ); + } else if (!doLength && objType !== "date" && !isNumeric(obj)) { + let printObj = objType === "string" ? "'" + obj + "'" : obj; + throw new AssertionError( + msgPrefix + "expected " + printObj + " to be a number or a date", + void 0, + ssfi + ); + } + if (doLength) { + let descriptor = "length", itemsCount; + if (objType === "map" || objType === "set") { + descriptor = "size"; + itemsCount = obj.size; + } else { + itemsCount = obj.length; + } + this.assert( + itemsCount > n, + "expected #{this} to have a " + descriptor + " above #{exp} but got #{act}", + "expected #{this} to not have a " + descriptor + " above #{exp}", + n, + itemsCount + ); + } else { + this.assert( + obj > n, + "expected #{this} to be above #{exp}", + "expected #{this} to be at most #{exp}", + n + ); + } +} +__name(assertAbove, "assertAbove"); +Assertion.addMethod("above", assertAbove); +Assertion.addMethod("gt", assertAbove); +Assertion.addMethod("greaterThan", assertAbove); +function assertLeast(n, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), doLength = flag2(this, "doLength"), flagMsg = flag2(this, "message"), msgPrefix = flagMsg ? flagMsg + ": " : "", ssfi = flag2(this, "ssfi"), objType = type(obj).toLowerCase(), nType = type(n).toLowerCase(), errorMessage, shouldThrow = true; + if (doLength && objType !== "map" && objType !== "set") { + new Assertion(obj, flagMsg, ssfi, true).to.have.property("length"); + } + if (!doLength && objType === "date" && nType !== "date") { + errorMessage = msgPrefix + "the argument to least must be a date"; + } else if (!isNumeric(n) && (doLength || isNumeric(obj))) { + errorMessage = msgPrefix + "the argument to least must be a number"; + } else if (!doLength && objType !== "date" && !isNumeric(obj)) { + let printObj = objType === "string" ? "'" + obj + "'" : obj; + errorMessage = msgPrefix + "expected " + printObj + " to be a number or a date"; + } else { + shouldThrow = false; + } + if (shouldThrow) { + throw new AssertionError(errorMessage, void 0, ssfi); + } + if (doLength) { + let descriptor = "length", itemsCount; + if (objType === "map" || objType === "set") { + descriptor = "size"; + itemsCount = obj.size; + } else { + itemsCount = obj.length; + } + this.assert( + itemsCount >= n, + "expected #{this} to have a " + descriptor + " at least #{exp} but got #{act}", + "expected #{this} to have a " + descriptor + " below #{exp}", + n, + itemsCount + ); + } else { + this.assert( + obj >= n, + "expected #{this} to be at least #{exp}", + "expected #{this} to be below #{exp}", + n + ); + } +} +__name(assertLeast, "assertLeast"); +Assertion.addMethod("least", assertLeast); +Assertion.addMethod("gte", assertLeast); +Assertion.addMethod("greaterThanOrEqual", assertLeast); +function assertBelow(n, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), doLength = flag2(this, "doLength"), flagMsg = flag2(this, "message"), msgPrefix = flagMsg ? flagMsg + ": " : "", ssfi = flag2(this, "ssfi"), objType = type(obj).toLowerCase(), nType = type(n).toLowerCase(), errorMessage, shouldThrow = true; + if (doLength && objType !== "map" && objType !== "set") { + new Assertion(obj, flagMsg, ssfi, true).to.have.property("length"); + } + if (!doLength && objType === "date" && nType !== "date") { + errorMessage = msgPrefix + "the argument to below must be a date"; + } else if (!isNumeric(n) && (doLength || isNumeric(obj))) { + errorMessage = msgPrefix + "the argument to below must be a number"; + } else if (!doLength && objType !== "date" && !isNumeric(obj)) { + let printObj = objType === "string" ? "'" + obj + "'" : obj; + errorMessage = msgPrefix + "expected " + printObj + " to be a number or a date"; + } else { + shouldThrow = false; + } + if (shouldThrow) { + throw new AssertionError(errorMessage, void 0, ssfi); + } + if (doLength) { + let descriptor = "length", itemsCount; + if (objType === "map" || objType === "set") { + descriptor = "size"; + itemsCount = obj.size; + } else { + itemsCount = obj.length; + } + this.assert( + itemsCount < n, + "expected #{this} to have a " + descriptor + " below #{exp} but got #{act}", + "expected #{this} to not have a " + descriptor + " below #{exp}", + n, + itemsCount + ); + } else { + this.assert( + obj < n, + "expected #{this} to be below #{exp}", + "expected #{this} to be at least #{exp}", + n + ); + } +} +__name(assertBelow, "assertBelow"); +Assertion.addMethod("below", assertBelow); +Assertion.addMethod("lt", assertBelow); +Assertion.addMethod("lessThan", assertBelow); +function assertMost(n, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), doLength = flag2(this, "doLength"), flagMsg = flag2(this, "message"), msgPrefix = flagMsg ? flagMsg + ": " : "", ssfi = flag2(this, "ssfi"), objType = type(obj).toLowerCase(), nType = type(n).toLowerCase(), errorMessage, shouldThrow = true; + if (doLength && objType !== "map" && objType !== "set") { + new Assertion(obj, flagMsg, ssfi, true).to.have.property("length"); + } + if (!doLength && objType === "date" && nType !== "date") { + errorMessage = msgPrefix + "the argument to most must be a date"; + } else if (!isNumeric(n) && (doLength || isNumeric(obj))) { + errorMessage = msgPrefix + "the argument to most must be a number"; + } else if (!doLength && objType !== "date" && !isNumeric(obj)) { + let printObj = objType === "string" ? "'" + obj + "'" : obj; + errorMessage = msgPrefix + "expected " + printObj + " to be a number or a date"; + } else { + shouldThrow = false; + } + if (shouldThrow) { + throw new AssertionError(errorMessage, void 0, ssfi); + } + if (doLength) { + let descriptor = "length", itemsCount; + if (objType === "map" || objType === "set") { + descriptor = "size"; + itemsCount = obj.size; + } else { + itemsCount = obj.length; + } + this.assert( + itemsCount <= n, + "expected #{this} to have a " + descriptor + " at most #{exp} but got #{act}", + "expected #{this} to have a " + descriptor + " above #{exp}", + n, + itemsCount + ); + } else { + this.assert( + obj <= n, + "expected #{this} to be at most #{exp}", + "expected #{this} to be above #{exp}", + n + ); + } +} +__name(assertMost, "assertMost"); +Assertion.addMethod("most", assertMost); +Assertion.addMethod("lte", assertMost); +Assertion.addMethod("lessThanOrEqual", assertMost); +Assertion.addMethod("within", function(start, finish, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), doLength = flag2(this, "doLength"), flagMsg = flag2(this, "message"), msgPrefix = flagMsg ? flagMsg + ": " : "", ssfi = flag2(this, "ssfi"), objType = type(obj).toLowerCase(), startType = type(start).toLowerCase(), finishType = type(finish).toLowerCase(), errorMessage, shouldThrow = true, range = startType === "date" && finishType === "date" ? start.toISOString() + ".." + finish.toISOString() : start + ".." + finish; + if (doLength && objType !== "map" && objType !== "set") { + new Assertion(obj, flagMsg, ssfi, true).to.have.property("length"); + } + if (!doLength && objType === "date" && (startType !== "date" || finishType !== "date")) { + errorMessage = msgPrefix + "the arguments to within must be dates"; + } else if ((!isNumeric(start) || !isNumeric(finish)) && (doLength || isNumeric(obj))) { + errorMessage = msgPrefix + "the arguments to within must be numbers"; + } else if (!doLength && objType !== "date" && !isNumeric(obj)) { + let printObj = objType === "string" ? "'" + obj + "'" : obj; + errorMessage = msgPrefix + "expected " + printObj + " to be a number or a date"; + } else { + shouldThrow = false; + } + if (shouldThrow) { + throw new AssertionError(errorMessage, void 0, ssfi); + } + if (doLength) { + let descriptor = "length", itemsCount; + if (objType === "map" || objType === "set") { + descriptor = "size"; + itemsCount = obj.size; + } else { + itemsCount = obj.length; + } + this.assert( + itemsCount >= start && itemsCount <= finish, + "expected #{this} to have a " + descriptor + " within " + range, + "expected #{this} to not have a " + descriptor + " within " + range + ); + } else { + this.assert( + obj >= start && obj <= finish, + "expected #{this} to be within " + range, + "expected #{this} to not be within " + range + ); + } +}); +function assertInstanceOf(constructor, msg) { + if (msg) flag2(this, "message", msg); + let target = flag2(this, "object"); + let ssfi = flag2(this, "ssfi"); + let flagMsg = flag2(this, "message"); + let isInstanceOf; + try { + isInstanceOf = target instanceof constructor; + } catch (err) { + if (err instanceof TypeError) { + flagMsg = flagMsg ? flagMsg + ": " : ""; + throw new AssertionError( + flagMsg + "The instanceof assertion needs a constructor but " + type(constructor) + " was given.", + void 0, + ssfi + ); + } + throw err; + } + let name = getName(constructor); + if (name == null) { + name = "an unnamed constructor"; + } + this.assert( + isInstanceOf, + "expected #{this} to be an instance of " + name, + "expected #{this} to not be an instance of " + name + ); +} +__name(assertInstanceOf, "assertInstanceOf"); +Assertion.addMethod("instanceof", assertInstanceOf); +Assertion.addMethod("instanceOf", assertInstanceOf); +function assertProperty(name, val, msg) { + if (msg) flag2(this, "message", msg); + let isNested = flag2(this, "nested"), isOwn = flag2(this, "own"), flagMsg = flag2(this, "message"), obj = flag2(this, "object"), ssfi = flag2(this, "ssfi"), nameType = typeof name; + flagMsg = flagMsg ? flagMsg + ": " : ""; + if (isNested) { + if (nameType !== "string") { + throw new AssertionError( + flagMsg + "the argument to property must be a string when using nested syntax", + void 0, + ssfi + ); + } + } else { + if (nameType !== "string" && nameType !== "number" && nameType !== "symbol") { + throw new AssertionError( + flagMsg + "the argument to property must be a string, number, or symbol", + void 0, + ssfi + ); + } + } + if (isNested && isOwn) { + throw new AssertionError( + flagMsg + 'The "nested" and "own" flags cannot be combined.', + void 0, + ssfi + ); + } + if (obj === null || obj === void 0) { + throw new AssertionError( + flagMsg + "Target cannot be null or undefined.", + void 0, + ssfi + ); + } + let isDeep = flag2(this, "deep"), negate = flag2(this, "negate"), pathInfo = isNested ? getPathInfo(obj, name) : null, value = isNested ? pathInfo.value : obj[name], isEql = isDeep ? flag2(this, "eql") : (val1, val2) => val1 === val2; + let descriptor = ""; + if (isDeep) descriptor += "deep "; + if (isOwn) descriptor += "own "; + if (isNested) descriptor += "nested "; + descriptor += "property "; + let hasProperty2; + if (isOwn) hasProperty2 = Object.prototype.hasOwnProperty.call(obj, name); + else if (isNested) hasProperty2 = pathInfo.exists; + else hasProperty2 = hasProperty(obj, name); + if (!negate || arguments.length === 1) { + this.assert( + hasProperty2, + "expected #{this} to have " + descriptor + inspect2(name), + "expected #{this} to not have " + descriptor + inspect2(name) + ); + } + if (arguments.length > 1) { + this.assert( + hasProperty2 && isEql(val, value), + "expected #{this} to have " + descriptor + inspect2(name) + " of #{exp}, but got #{act}", + "expected #{this} to not have " + descriptor + inspect2(name) + " of #{act}", + val, + value + ); + } + flag2(this, "object", value); +} +__name(assertProperty, "assertProperty"); +Assertion.addMethod("property", assertProperty); +function assertOwnProperty(_name, _value, _msg) { + flag2(this, "own", true); + assertProperty.apply(this, arguments); +} +__name(assertOwnProperty, "assertOwnProperty"); +Assertion.addMethod("ownProperty", assertOwnProperty); +Assertion.addMethod("haveOwnProperty", assertOwnProperty); +function assertOwnPropertyDescriptor(name, descriptor, msg) { + if (typeof descriptor === "string") { + msg = descriptor; + descriptor = null; + } + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"); + let actualDescriptor = Object.getOwnPropertyDescriptor(Object(obj), name); + let eql = flag2(this, "eql"); + if (actualDescriptor && descriptor) { + this.assert( + eql(descriptor, actualDescriptor), + "expected the own property descriptor for " + inspect2(name) + " on #{this} to match " + inspect2(descriptor) + ", got " + inspect2(actualDescriptor), + "expected the own property descriptor for " + inspect2(name) + " on #{this} to not match " + inspect2(descriptor), + descriptor, + actualDescriptor, + true + ); + } else { + this.assert( + actualDescriptor, + "expected #{this} to have an own property descriptor for " + inspect2(name), + "expected #{this} to not have an own property descriptor for " + inspect2(name) + ); + } + flag2(this, "object", actualDescriptor); +} +__name(assertOwnPropertyDescriptor, "assertOwnPropertyDescriptor"); +Assertion.addMethod("ownPropertyDescriptor", assertOwnPropertyDescriptor); +Assertion.addMethod("haveOwnPropertyDescriptor", assertOwnPropertyDescriptor); +function assertLengthChain() { + flag2(this, "doLength", true); +} +__name(assertLengthChain, "assertLengthChain"); +function assertLength(n, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), objType = type(obj).toLowerCase(), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"), descriptor = "length", itemsCount; + switch (objType) { + case "map": + case "set": + descriptor = "size"; + itemsCount = obj.size; + break; + default: + new Assertion(obj, flagMsg, ssfi, true).to.have.property("length"); + itemsCount = obj.length; + } + this.assert( + itemsCount == n, + "expected #{this} to have a " + descriptor + " of #{exp} but got #{act}", + "expected #{this} to not have a " + descriptor + " of #{act}", + n, + itemsCount + ); +} +__name(assertLength, "assertLength"); +Assertion.addChainableMethod("length", assertLength, assertLengthChain); +Assertion.addChainableMethod("lengthOf", assertLength, assertLengthChain); +function assertMatch(re, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"); + this.assert( + re.exec(obj), + "expected #{this} to match " + re, + "expected #{this} not to match " + re + ); +} +__name(assertMatch, "assertMatch"); +Assertion.addMethod("match", assertMatch); +Assertion.addMethod("matches", assertMatch); +Assertion.addMethod("string", function(str, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"); + new Assertion(obj, flagMsg, ssfi, true).is.a("string"); + this.assert( + ~obj.indexOf(str), + "expected #{this} to contain " + inspect2(str), + "expected #{this} to not contain " + inspect2(str) + ); +}); +function assertKeys(keys) { + let obj = flag2(this, "object"), objType = type(obj), keysType = type(keys), ssfi = flag2(this, "ssfi"), isDeep = flag2(this, "deep"), str, deepStr = "", actual, ok = true, flagMsg = flag2(this, "message"); + flagMsg = flagMsg ? flagMsg + ": " : ""; + let mixedArgsMsg = flagMsg + "when testing keys against an object or an array you must give a single Array|Object|String argument or multiple String arguments"; + if (objType === "Map" || objType === "Set") { + deepStr = isDeep ? "deeply " : ""; + actual = []; + obj.forEach(function(val, key) { + actual.push(key); + }); + if (keysType !== "Array") { + keys = Array.prototype.slice.call(arguments); + } + } else { + actual = getOwnEnumerableProperties(obj); + switch (keysType) { + case "Array": + if (arguments.length > 1) { + throw new AssertionError(mixedArgsMsg, void 0, ssfi); + } + break; + case "Object": + if (arguments.length > 1) { + throw new AssertionError(mixedArgsMsg, void 0, ssfi); + } + keys = Object.keys(keys); + break; + default: + keys = Array.prototype.slice.call(arguments); + } + keys = keys.map(function(val) { + return typeof val === "symbol" ? val : String(val); + }); + } + if (!keys.length) { + throw new AssertionError(flagMsg + "keys required", void 0, ssfi); + } + let len = keys.length, any = flag2(this, "any"), all = flag2(this, "all"), expected = keys, isEql = isDeep ? flag2(this, "eql") : (val1, val2) => val1 === val2; + if (!any && !all) { + all = true; + } + if (any) { + ok = expected.some(function(expectedKey) { + return actual.some(function(actualKey) { + return isEql(expectedKey, actualKey); + }); + }); + } + if (all) { + ok = expected.every(function(expectedKey) { + return actual.some(function(actualKey) { + return isEql(expectedKey, actualKey); + }); + }); + if (!flag2(this, "contains")) { + ok = ok && keys.length == actual.length; + } + } + if (len > 1) { + keys = keys.map(function(key) { + return inspect2(key); + }); + let last = keys.pop(); + if (all) { + str = keys.join(", ") + ", and " + last; + } + if (any) { + str = keys.join(", ") + ", or " + last; + } + } else { + str = inspect2(keys[0]); + } + str = (len > 1 ? "keys " : "key ") + str; + str = (flag2(this, "contains") ? "contain " : "have ") + str; + this.assert( + ok, + "expected #{this} to " + deepStr + str, + "expected #{this} to not " + deepStr + str, + expected.slice(0).sort(compareByInspect), + actual.sort(compareByInspect), + true + ); +} +__name(assertKeys, "assertKeys"); +Assertion.addMethod("keys", assertKeys); +Assertion.addMethod("key", assertKeys); +function assertThrows(errorLike, errMsgMatcher, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), ssfi = flag2(this, "ssfi"), flagMsg = flag2(this, "message"), negate = flag2(this, "negate") || false; + new Assertion(obj, flagMsg, ssfi, true).is.a("function"); + if (isRegExp2(errorLike) || typeof errorLike === "string") { + errMsgMatcher = errorLike; + errorLike = null; + } + let caughtErr; + let errorWasThrown = false; + try { + obj(); + } catch (err) { + errorWasThrown = true; + caughtErr = err; + } + let everyArgIsUndefined = errorLike === void 0 && errMsgMatcher === void 0; + let everyArgIsDefined = Boolean(errorLike && errMsgMatcher); + let errorLikeFail = false; + let errMsgMatcherFail = false; + if (everyArgIsUndefined || !everyArgIsUndefined && !negate) { + let errorLikeString = "an error"; + if (errorLike instanceof Error) { + errorLikeString = "#{exp}"; + } else if (errorLike) { + errorLikeString = check_error_exports.getConstructorName(errorLike); + } + let actual = caughtErr; + if (caughtErr instanceof Error) { + actual = caughtErr.toString(); + } else if (typeof caughtErr === "string") { + actual = caughtErr; + } else if (caughtErr && (typeof caughtErr === "object" || typeof caughtErr === "function")) { + try { + actual = check_error_exports.getConstructorName(caughtErr); + } catch (_err) { + } + } + this.assert( + errorWasThrown, + "expected #{this} to throw " + errorLikeString, + "expected #{this} to not throw an error but #{act} was thrown", + errorLike && errorLike.toString(), + actual + ); + } + if (errorLike && caughtErr) { + if (errorLike instanceof Error) { + let isCompatibleInstance = check_error_exports.compatibleInstance( + caughtErr, + errorLike + ); + if (isCompatibleInstance === negate) { + if (everyArgIsDefined && negate) { + errorLikeFail = true; + } else { + this.assert( + negate, + "expected #{this} to throw #{exp} but #{act} was thrown", + "expected #{this} to not throw #{exp}" + (caughtErr && !negate ? " but #{act} was thrown" : ""), + errorLike.toString(), + caughtErr.toString() + ); + } + } + } + let isCompatibleConstructor = check_error_exports.compatibleConstructor( + caughtErr, + errorLike + ); + if (isCompatibleConstructor === negate) { + if (everyArgIsDefined && negate) { + errorLikeFail = true; + } else { + this.assert( + negate, + "expected #{this} to throw #{exp} but #{act} was thrown", + "expected #{this} to not throw #{exp}" + (caughtErr ? " but #{act} was thrown" : ""), + errorLike instanceof Error ? errorLike.toString() : errorLike && check_error_exports.getConstructorName(errorLike), + caughtErr instanceof Error ? caughtErr.toString() : caughtErr && check_error_exports.getConstructorName(caughtErr) + ); + } + } + } + if (caughtErr && errMsgMatcher !== void 0 && errMsgMatcher !== null) { + let placeholder = "including"; + if (isRegExp2(errMsgMatcher)) { + placeholder = "matching"; + } + let isCompatibleMessage = check_error_exports.compatibleMessage( + caughtErr, + errMsgMatcher + ); + if (isCompatibleMessage === negate) { + if (everyArgIsDefined && negate) { + errMsgMatcherFail = true; + } else { + this.assert( + negate, + "expected #{this} to throw error " + placeholder + " #{exp} but got #{act}", + "expected #{this} to throw error not " + placeholder + " #{exp}", + errMsgMatcher, + check_error_exports.getMessage(caughtErr) + ); + } + } + } + if (errorLikeFail && errMsgMatcherFail) { + this.assert( + negate, + "expected #{this} to throw #{exp} but #{act} was thrown", + "expected #{this} to not throw #{exp}" + (caughtErr ? " but #{act} was thrown" : ""), + errorLike instanceof Error ? errorLike.toString() : errorLike && check_error_exports.getConstructorName(errorLike), + caughtErr instanceof Error ? caughtErr.toString() : caughtErr && check_error_exports.getConstructorName(caughtErr) + ); + } + flag2(this, "object", caughtErr); +} +__name(assertThrows, "assertThrows"); +Assertion.addMethod("throw", assertThrows); +Assertion.addMethod("throws", assertThrows); +Assertion.addMethod("Throw", assertThrows); +function respondTo(method, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), itself = flag2(this, "itself"), context = "function" === typeof obj && !itself ? obj.prototype[method] : obj[method]; + this.assert( + "function" === typeof context, + "expected #{this} to respond to " + inspect2(method), + "expected #{this} to not respond to " + inspect2(method) + ); +} +__name(respondTo, "respondTo"); +Assertion.addMethod("respondTo", respondTo); +Assertion.addMethod("respondsTo", respondTo); +Assertion.addProperty("itself", function() { + flag2(this, "itself", true); +}); +function satisfy(matcher, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"); + let result = matcher(obj); + this.assert( + result, + "expected #{this} to satisfy " + objDisplay(matcher), + "expected #{this} to not satisfy" + objDisplay(matcher), + flag2(this, "negate") ? false : true, + result + ); +} +__name(satisfy, "satisfy"); +Assertion.addMethod("satisfy", satisfy); +Assertion.addMethod("satisfies", satisfy); +function closeTo(expected, delta, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"); + new Assertion(obj, flagMsg, ssfi, true).is.numeric; + let message = "A `delta` value is required for `closeTo`"; + if (delta == void 0) { + throw new AssertionError( + flagMsg ? `${flagMsg}: ${message}` : message, + void 0, + ssfi + ); + } + new Assertion(delta, flagMsg, ssfi, true).is.numeric; + message = "A `expected` value is required for `closeTo`"; + if (expected == void 0) { + throw new AssertionError( + flagMsg ? `${flagMsg}: ${message}` : message, + void 0, + ssfi + ); + } + new Assertion(expected, flagMsg, ssfi, true).is.numeric; + const abs = /* @__PURE__ */ __name((x) => x < 0n ? -x : x, "abs"); + const strip = /* @__PURE__ */ __name((number) => parseFloat(parseFloat(number).toPrecision(12)), "strip"); + this.assert( + strip(abs(obj - expected)) <= delta, + "expected #{this} to be close to " + expected + " +/- " + delta, + "expected #{this} not to be close to " + expected + " +/- " + delta + ); +} +__name(closeTo, "closeTo"); +Assertion.addMethod("closeTo", closeTo); +Assertion.addMethod("approximately", closeTo); +function isSubsetOf(_subset, _superset, cmp, contains, ordered) { + let superset = Array.from(_superset); + let subset = Array.from(_subset); + if (!contains) { + if (subset.length !== superset.length) return false; + superset = superset.slice(); + } + return subset.every(function(elem, idx) { + if (ordered) return cmp ? cmp(elem, superset[idx]) : elem === superset[idx]; + if (!cmp) { + let matchIdx = superset.indexOf(elem); + if (matchIdx === -1) return false; + if (!contains) superset.splice(matchIdx, 1); + return true; + } + return superset.some(function(elem2, matchIdx) { + if (!cmp(elem, elem2)) return false; + if (!contains) superset.splice(matchIdx, 1); + return true; + }); + }); +} +__name(isSubsetOf, "isSubsetOf"); +Assertion.addMethod("members", function(subset, msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"); + new Assertion(obj, flagMsg, ssfi, true).to.be.iterable; + new Assertion(subset, flagMsg, ssfi, true).to.be.iterable; + let contains = flag2(this, "contains"); + let ordered = flag2(this, "ordered"); + let subject, failMsg, failNegateMsg; + if (contains) { + subject = ordered ? "an ordered superset" : "a superset"; + failMsg = "expected #{this} to be " + subject + " of #{exp}"; + failNegateMsg = "expected #{this} to not be " + subject + " of #{exp}"; + } else { + subject = ordered ? "ordered members" : "members"; + failMsg = "expected #{this} to have the same " + subject + " as #{exp}"; + failNegateMsg = "expected #{this} to not have the same " + subject + " as #{exp}"; + } + let cmp = flag2(this, "deep") ? flag2(this, "eql") : void 0; + this.assert( + isSubsetOf(subset, obj, cmp, contains, ordered), + failMsg, + failNegateMsg, + subset, + obj, + true + ); +}); +Assertion.addProperty("iterable", function(msg) { + if (msg) flag2(this, "message", msg); + let obj = flag2(this, "object"); + this.assert( + obj != void 0 && obj[Symbol.iterator], + "expected #{this} to be an iterable", + "expected #{this} to not be an iterable", + obj + ); +}); +function oneOf(list, msg) { + if (msg) flag2(this, "message", msg); + let expected = flag2(this, "object"), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"), contains = flag2(this, "contains"), isDeep = flag2(this, "deep"), eql = flag2(this, "eql"); + new Assertion(list, flagMsg, ssfi, true).to.be.an("array"); + if (contains) { + this.assert( + list.some(function(possibility) { + return expected.indexOf(possibility) > -1; + }), + "expected #{this} to contain one of #{exp}", + "expected #{this} to not contain one of #{exp}", + list, + expected + ); + } else { + if (isDeep) { + this.assert( + list.some(function(possibility) { + return eql(expected, possibility); + }), + "expected #{this} to deeply equal one of #{exp}", + "expected #{this} to deeply equal one of #{exp}", + list, + expected + ); + } else { + this.assert( + list.indexOf(expected) > -1, + "expected #{this} to be one of #{exp}", + "expected #{this} to not be one of #{exp}", + list, + expected + ); + } + } +} +__name(oneOf, "oneOf"); +Assertion.addMethod("oneOf", oneOf); +function assertChanges(subject, prop, msg) { + if (msg) flag2(this, "message", msg); + let fn = flag2(this, "object"), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"); + new Assertion(fn, flagMsg, ssfi, true).is.a("function"); + let initial; + if (!prop) { + new Assertion(subject, flagMsg, ssfi, true).is.a("function"); + initial = subject(); + } else { + new Assertion(subject, flagMsg, ssfi, true).to.have.property(prop); + initial = subject[prop]; + } + fn(); + let final = prop === void 0 || prop === null ? subject() : subject[prop]; + let msgObj = prop === void 0 || prop === null ? initial : "." + prop; + flag2(this, "deltaMsgObj", msgObj); + flag2(this, "initialDeltaValue", initial); + flag2(this, "finalDeltaValue", final); + flag2(this, "deltaBehavior", "change"); + flag2(this, "realDelta", final !== initial); + this.assert( + initial !== final, + "expected " + msgObj + " to change", + "expected " + msgObj + " to not change" + ); +} +__name(assertChanges, "assertChanges"); +Assertion.addMethod("change", assertChanges); +Assertion.addMethod("changes", assertChanges); +function assertIncreases(subject, prop, msg) { + if (msg) flag2(this, "message", msg); + let fn = flag2(this, "object"), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"); + new Assertion(fn, flagMsg, ssfi, true).is.a("function"); + let initial; + if (!prop) { + new Assertion(subject, flagMsg, ssfi, true).is.a("function"); + initial = subject(); + } else { + new Assertion(subject, flagMsg, ssfi, true).to.have.property(prop); + initial = subject[prop]; + } + new Assertion(initial, flagMsg, ssfi, true).is.a("number"); + fn(); + let final = prop === void 0 || prop === null ? subject() : subject[prop]; + let msgObj = prop === void 0 || prop === null ? initial : "." + prop; + flag2(this, "deltaMsgObj", msgObj); + flag2(this, "initialDeltaValue", initial); + flag2(this, "finalDeltaValue", final); + flag2(this, "deltaBehavior", "increase"); + flag2(this, "realDelta", final - initial); + this.assert( + final - initial > 0, + "expected " + msgObj + " to increase", + "expected " + msgObj + " to not increase" + ); +} +__name(assertIncreases, "assertIncreases"); +Assertion.addMethod("increase", assertIncreases); +Assertion.addMethod("increases", assertIncreases); +function assertDecreases(subject, prop, msg) { + if (msg) flag2(this, "message", msg); + let fn = flag2(this, "object"), flagMsg = flag2(this, "message"), ssfi = flag2(this, "ssfi"); + new Assertion(fn, flagMsg, ssfi, true).is.a("function"); + let initial; + if (!prop) { + new Assertion(subject, flagMsg, ssfi, true).is.a("function"); + initial = subject(); + } else { + new Assertion(subject, flagMsg, ssfi, true).to.have.property(prop); + initial = subject[prop]; + } + new Assertion(initial, flagMsg, ssfi, true).is.a("number"); + fn(); + let final = prop === void 0 || prop === null ? subject() : subject[prop]; + let msgObj = prop === void 0 || prop === null ? initial : "." + prop; + flag2(this, "deltaMsgObj", msgObj); + flag2(this, "initialDeltaValue", initial); + flag2(this, "finalDeltaValue", final); + flag2(this, "deltaBehavior", "decrease"); + flag2(this, "realDelta", initial - final); + this.assert( + final - initial < 0, + "expected " + msgObj + " to decrease", + "expected " + msgObj + " to not decrease" + ); +} +__name(assertDecreases, "assertDecreases"); +Assertion.addMethod("decrease", assertDecreases); +Assertion.addMethod("decreases", assertDecreases); +function assertDelta(delta, msg) { + if (msg) flag2(this, "message", msg); + let msgObj = flag2(this, "deltaMsgObj"); + let initial = flag2(this, "initialDeltaValue"); + let final = flag2(this, "finalDeltaValue"); + let behavior = flag2(this, "deltaBehavior"); + let realDelta = flag2(this, "realDelta"); + let expression; + if (behavior === "change") { + expression = Math.abs(final - initial) === Math.abs(delta); + } else { + expression = realDelta === Math.abs(delta); + } + this.assert( + expression, + "expected " + msgObj + " to " + behavior + " by " + delta, + "expected " + msgObj + " to not " + behavior + " by " + delta + ); +} +__name(assertDelta, "assertDelta"); +Assertion.addMethod("by", assertDelta); +Assertion.addProperty("extensible", function() { + let obj = flag2(this, "object"); + let isExtensible = obj === Object(obj) && Object.isExtensible(obj); + this.assert( + isExtensible, + "expected #{this} to be extensible", + "expected #{this} to not be extensible" + ); +}); +Assertion.addProperty("sealed", function() { + let obj = flag2(this, "object"); + let isSealed = obj === Object(obj) ? Object.isSealed(obj) : true; + this.assert( + isSealed, + "expected #{this} to be sealed", + "expected #{this} to not be sealed" + ); +}); +Assertion.addProperty("frozen", function() { + let obj = flag2(this, "object"); + let isFrozen = obj === Object(obj) ? Object.isFrozen(obj) : true; + this.assert( + isFrozen, + "expected #{this} to be frozen", + "expected #{this} to not be frozen" + ); +}); +Assertion.addProperty("finite", function(_msg) { + let obj = flag2(this, "object"); + this.assert( + typeof obj === "number" && isFinite(obj), + "expected #{this} to be a finite number", + "expected #{this} to not be a finite number" + ); +}); +function compareSubset(expected, actual) { + if (expected === actual) { + return true; + } + if (typeof actual !== typeof expected) { + return false; + } + if (typeof expected !== "object" || expected === null) { + return expected === actual; + } + if (!actual) { + return false; + } + if (Array.isArray(expected)) { + if (!Array.isArray(actual)) { + return false; + } + return expected.every(function(exp) { + return actual.some(function(act) { + return compareSubset(exp, act); + }); + }); + } + if (expected instanceof Date) { + if (actual instanceof Date) { + return expected.getTime() === actual.getTime(); + } else { + return false; + } + } + return Object.keys(expected).every(function(key) { + let expectedValue = expected[key]; + let actualValue = actual[key]; + if (typeof expectedValue === "object" && expectedValue !== null && actualValue !== null) { + return compareSubset(expectedValue, actualValue); + } + if (typeof expectedValue === "function") { + return expectedValue(actualValue); + } + return actualValue === expectedValue; + }); +} +__name(compareSubset, "compareSubset"); +Assertion.addMethod("containSubset", function(expected) { + const actual = flag(this, "object"); + const showDiff = config.showDiff; + this.assert( + compareSubset(expected, actual), + "expected #{act} to contain subset #{exp}", + "expected #{act} to not contain subset #{exp}", + expected, + actual, + showDiff + ); +}); + +// lib/chai/interface/expect.js +function expect(val, message) { + return new Assertion(val, message); +} +__name(expect, "expect"); +expect.fail = function(actual, expected, message, operator) { + if (arguments.length < 2) { + message = actual; + actual = void 0; + } + message = message || "expect.fail()"; + throw new AssertionError( + message, + { + actual, + expected, + operator + }, + expect.fail + ); +}; + +// lib/chai/interface/should.js +var should_exports = {}; +__export(should_exports, { + Should: () => Should, + should: () => should +}); +function loadShould() { + function shouldGetter() { + if (this instanceof String || this instanceof Number || this instanceof Boolean || typeof Symbol === "function" && this instanceof Symbol || typeof BigInt === "function" && this instanceof BigInt) { + return new Assertion(this.valueOf(), null, shouldGetter); + } + return new Assertion(this, null, shouldGetter); + } + __name(shouldGetter, "shouldGetter"); + function shouldSetter(value) { + Object.defineProperty(this, "should", { + value, + enumerable: true, + configurable: true, + writable: true + }); + } + __name(shouldSetter, "shouldSetter"); + Object.defineProperty(Object.prototype, "should", { + set: shouldSetter, + get: shouldGetter, + configurable: true + }); + let should2 = {}; + should2.fail = function(actual, expected, message, operator) { + if (arguments.length < 2) { + message = actual; + actual = void 0; + } + message = message || "should.fail()"; + throw new AssertionError( + message, + { + actual, + expected, + operator + }, + should2.fail + ); + }; + should2.equal = function(actual, expected, message) { + new Assertion(actual, message).to.equal(expected); + }; + should2.Throw = function(fn, errt, errs, msg) { + new Assertion(fn, msg).to.Throw(errt, errs); + }; + should2.exist = function(val, msg) { + new Assertion(val, msg).to.exist; + }; + should2.not = {}; + should2.not.equal = function(actual, expected, msg) { + new Assertion(actual, msg).to.not.equal(expected); + }; + should2.not.Throw = function(fn, errt, errs, msg) { + new Assertion(fn, msg).to.not.Throw(errt, errs); + }; + should2.not.exist = function(val, msg) { + new Assertion(val, msg).to.not.exist; + }; + should2["throw"] = should2["Throw"]; + should2.not["throw"] = should2.not["Throw"]; + return should2; +} +__name(loadShould, "loadShould"); +var should = loadShould; +var Should = loadShould; + +// lib/chai/interface/assert.js +function assert(express, errmsg) { + let test2 = new Assertion(null, null, assert, true); + test2.assert(express, errmsg, "[ negation message unavailable ]"); +} +__name(assert, "assert"); +assert.fail = function(actual, expected, message, operator) { + if (arguments.length < 2) { + message = actual; + actual = void 0; + } + message = message || "assert.fail()"; + throw new AssertionError( + message, + { + actual, + expected, + operator + }, + assert.fail + ); +}; +assert.isOk = function(val, msg) { + new Assertion(val, msg, assert.isOk, true).is.ok; +}; +assert.isNotOk = function(val, msg) { + new Assertion(val, msg, assert.isNotOk, true).is.not.ok; +}; +assert.equal = function(act, exp, msg) { + let test2 = new Assertion(act, msg, assert.equal, true); + test2.assert( + exp == flag(test2, "object"), + "expected #{this} to equal #{exp}", + "expected #{this} to not equal #{act}", + exp, + act, + true + ); +}; +assert.notEqual = function(act, exp, msg) { + let test2 = new Assertion(act, msg, assert.notEqual, true); + test2.assert( + exp != flag(test2, "object"), + "expected #{this} to not equal #{exp}", + "expected #{this} to equal #{act}", + exp, + act, + true + ); +}; +assert.strictEqual = function(act, exp, msg) { + new Assertion(act, msg, assert.strictEqual, true).to.equal(exp); +}; +assert.notStrictEqual = function(act, exp, msg) { + new Assertion(act, msg, assert.notStrictEqual, true).to.not.equal(exp); +}; +assert.deepEqual = assert.deepStrictEqual = function(act, exp, msg) { + new Assertion(act, msg, assert.deepEqual, true).to.eql(exp); +}; +assert.notDeepEqual = function(act, exp, msg) { + new Assertion(act, msg, assert.notDeepEqual, true).to.not.eql(exp); +}; +assert.isAbove = function(val, abv, msg) { + new Assertion(val, msg, assert.isAbove, true).to.be.above(abv); +}; +assert.isAtLeast = function(val, atlst, msg) { + new Assertion(val, msg, assert.isAtLeast, true).to.be.least(atlst); +}; +assert.isBelow = function(val, blw, msg) { + new Assertion(val, msg, assert.isBelow, true).to.be.below(blw); +}; +assert.isAtMost = function(val, atmst, msg) { + new Assertion(val, msg, assert.isAtMost, true).to.be.most(atmst); +}; +assert.isTrue = function(val, msg) { + new Assertion(val, msg, assert.isTrue, true).is["true"]; +}; +assert.isNotTrue = function(val, msg) { + new Assertion(val, msg, assert.isNotTrue, true).to.not.equal(true); +}; +assert.isFalse = function(val, msg) { + new Assertion(val, msg, assert.isFalse, true).is["false"]; +}; +assert.isNotFalse = function(val, msg) { + new Assertion(val, msg, assert.isNotFalse, true).to.not.equal(false); +}; +assert.isNull = function(val, msg) { + new Assertion(val, msg, assert.isNull, true).to.equal(null); +}; +assert.isNotNull = function(val, msg) { + new Assertion(val, msg, assert.isNotNull, true).to.not.equal(null); +}; +assert.isNaN = function(val, msg) { + new Assertion(val, msg, assert.isNaN, true).to.be.NaN; +}; +assert.isNotNaN = function(value, message) { + new Assertion(value, message, assert.isNotNaN, true).not.to.be.NaN; +}; +assert.exists = function(val, msg) { + new Assertion(val, msg, assert.exists, true).to.exist; +}; +assert.notExists = function(val, msg) { + new Assertion(val, msg, assert.notExists, true).to.not.exist; +}; +assert.isUndefined = function(val, msg) { + new Assertion(val, msg, assert.isUndefined, true).to.equal(void 0); +}; +assert.isDefined = function(val, msg) { + new Assertion(val, msg, assert.isDefined, true).to.not.equal(void 0); +}; +assert.isCallable = function(value, message) { + new Assertion(value, message, assert.isCallable, true).is.callable; +}; +assert.isNotCallable = function(value, message) { + new Assertion(value, message, assert.isNotCallable, true).is.not.callable; +}; +assert.isObject = function(val, msg) { + new Assertion(val, msg, assert.isObject, true).to.be.a("object"); +}; +assert.isNotObject = function(val, msg) { + new Assertion(val, msg, assert.isNotObject, true).to.not.be.a("object"); +}; +assert.isArray = function(val, msg) { + new Assertion(val, msg, assert.isArray, true).to.be.an("array"); +}; +assert.isNotArray = function(val, msg) { + new Assertion(val, msg, assert.isNotArray, true).to.not.be.an("array"); +}; +assert.isString = function(val, msg) { + new Assertion(val, msg, assert.isString, true).to.be.a("string"); +}; +assert.isNotString = function(val, msg) { + new Assertion(val, msg, assert.isNotString, true).to.not.be.a("string"); +}; +assert.isNumber = function(val, msg) { + new Assertion(val, msg, assert.isNumber, true).to.be.a("number"); +}; +assert.isNotNumber = function(val, msg) { + new Assertion(val, msg, assert.isNotNumber, true).to.not.be.a("number"); +}; +assert.isNumeric = function(val, msg) { + new Assertion(val, msg, assert.isNumeric, true).is.numeric; +}; +assert.isNotNumeric = function(val, msg) { + new Assertion(val, msg, assert.isNotNumeric, true).is.not.numeric; +}; +assert.isFinite = function(val, msg) { + new Assertion(val, msg, assert.isFinite, true).to.be.finite; +}; +assert.isBoolean = function(val, msg) { + new Assertion(val, msg, assert.isBoolean, true).to.be.a("boolean"); +}; +assert.isNotBoolean = function(val, msg) { + new Assertion(val, msg, assert.isNotBoolean, true).to.not.be.a("boolean"); +}; +assert.typeOf = function(val, type3, msg) { + new Assertion(val, msg, assert.typeOf, true).to.be.a(type3); +}; +assert.notTypeOf = function(value, type3, message) { + new Assertion(value, message, assert.notTypeOf, true).to.not.be.a(type3); +}; +assert.instanceOf = function(val, type3, msg) { + new Assertion(val, msg, assert.instanceOf, true).to.be.instanceOf(type3); +}; +assert.notInstanceOf = function(val, type3, msg) { + new Assertion(val, msg, assert.notInstanceOf, true).to.not.be.instanceOf( + type3 + ); +}; +assert.include = function(exp, inc, msg) { + new Assertion(exp, msg, assert.include, true).include(inc); +}; +assert.notInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.notInclude, true).not.include(inc); +}; +assert.deepInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.deepInclude, true).deep.include(inc); +}; +assert.notDeepInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.notDeepInclude, true).not.deep.include(inc); +}; +assert.nestedInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.nestedInclude, true).nested.include(inc); +}; +assert.notNestedInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.notNestedInclude, true).not.nested.include( + inc + ); +}; +assert.deepNestedInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.deepNestedInclude, true).deep.nested.include( + inc + ); +}; +assert.notDeepNestedInclude = function(exp, inc, msg) { + new Assertion( + exp, + msg, + assert.notDeepNestedInclude, + true + ).not.deep.nested.include(inc); +}; +assert.ownInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.ownInclude, true).own.include(inc); +}; +assert.notOwnInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.notOwnInclude, true).not.own.include(inc); +}; +assert.deepOwnInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.deepOwnInclude, true).deep.own.include(inc); +}; +assert.notDeepOwnInclude = function(exp, inc, msg) { + new Assertion(exp, msg, assert.notDeepOwnInclude, true).not.deep.own.include( + inc + ); +}; +assert.match = function(exp, re, msg) { + new Assertion(exp, msg, assert.match, true).to.match(re); +}; +assert.notMatch = function(exp, re, msg) { + new Assertion(exp, msg, assert.notMatch, true).to.not.match(re); +}; +assert.property = function(obj, prop, msg) { + new Assertion(obj, msg, assert.property, true).to.have.property(prop); +}; +assert.notProperty = function(obj, prop, msg) { + new Assertion(obj, msg, assert.notProperty, true).to.not.have.property(prop); +}; +assert.propertyVal = function(obj, prop, val, msg) { + new Assertion(obj, msg, assert.propertyVal, true).to.have.property(prop, val); +}; +assert.notPropertyVal = function(obj, prop, val, msg) { + new Assertion(obj, msg, assert.notPropertyVal, true).to.not.have.property( + prop, + val + ); +}; +assert.deepPropertyVal = function(obj, prop, val, msg) { + new Assertion(obj, msg, assert.deepPropertyVal, true).to.have.deep.property( + prop, + val + ); +}; +assert.notDeepPropertyVal = function(obj, prop, val, msg) { + new Assertion( + obj, + msg, + assert.notDeepPropertyVal, + true + ).to.not.have.deep.property(prop, val); +}; +assert.ownProperty = function(obj, prop, msg) { + new Assertion(obj, msg, assert.ownProperty, true).to.have.own.property(prop); +}; +assert.notOwnProperty = function(obj, prop, msg) { + new Assertion(obj, msg, assert.notOwnProperty, true).to.not.have.own.property( + prop + ); +}; +assert.ownPropertyVal = function(obj, prop, value, msg) { + new Assertion(obj, msg, assert.ownPropertyVal, true).to.have.own.property( + prop, + value + ); +}; +assert.notOwnPropertyVal = function(obj, prop, value, msg) { + new Assertion( + obj, + msg, + assert.notOwnPropertyVal, + true + ).to.not.have.own.property(prop, value); +}; +assert.deepOwnPropertyVal = function(obj, prop, value, msg) { + new Assertion( + obj, + msg, + assert.deepOwnPropertyVal, + true + ).to.have.deep.own.property(prop, value); +}; +assert.notDeepOwnPropertyVal = function(obj, prop, value, msg) { + new Assertion( + obj, + msg, + assert.notDeepOwnPropertyVal, + true + ).to.not.have.deep.own.property(prop, value); +}; +assert.nestedProperty = function(obj, prop, msg) { + new Assertion(obj, msg, assert.nestedProperty, true).to.have.nested.property( + prop + ); +}; +assert.notNestedProperty = function(obj, prop, msg) { + new Assertion( + obj, + msg, + assert.notNestedProperty, + true + ).to.not.have.nested.property(prop); +}; +assert.nestedPropertyVal = function(obj, prop, val, msg) { + new Assertion( + obj, + msg, + assert.nestedPropertyVal, + true + ).to.have.nested.property(prop, val); +}; +assert.notNestedPropertyVal = function(obj, prop, val, msg) { + new Assertion( + obj, + msg, + assert.notNestedPropertyVal, + true + ).to.not.have.nested.property(prop, val); +}; +assert.deepNestedPropertyVal = function(obj, prop, val, msg) { + new Assertion( + obj, + msg, + assert.deepNestedPropertyVal, + true + ).to.have.deep.nested.property(prop, val); +}; +assert.notDeepNestedPropertyVal = function(obj, prop, val, msg) { + new Assertion( + obj, + msg, + assert.notDeepNestedPropertyVal, + true + ).to.not.have.deep.nested.property(prop, val); +}; +assert.lengthOf = function(exp, len, msg) { + new Assertion(exp, msg, assert.lengthOf, true).to.have.lengthOf(len); +}; +assert.hasAnyKeys = function(obj, keys, msg) { + new Assertion(obj, msg, assert.hasAnyKeys, true).to.have.any.keys(keys); +}; +assert.hasAllKeys = function(obj, keys, msg) { + new Assertion(obj, msg, assert.hasAllKeys, true).to.have.all.keys(keys); +}; +assert.containsAllKeys = function(obj, keys, msg) { + new Assertion(obj, msg, assert.containsAllKeys, true).to.contain.all.keys( + keys + ); +}; +assert.doesNotHaveAnyKeys = function(obj, keys, msg) { + new Assertion(obj, msg, assert.doesNotHaveAnyKeys, true).to.not.have.any.keys( + keys + ); +}; +assert.doesNotHaveAllKeys = function(obj, keys, msg) { + new Assertion(obj, msg, assert.doesNotHaveAllKeys, true).to.not.have.all.keys( + keys + ); +}; +assert.hasAnyDeepKeys = function(obj, keys, msg) { + new Assertion(obj, msg, assert.hasAnyDeepKeys, true).to.have.any.deep.keys( + keys + ); +}; +assert.hasAllDeepKeys = function(obj, keys, msg) { + new Assertion(obj, msg, assert.hasAllDeepKeys, true).to.have.all.deep.keys( + keys + ); +}; +assert.containsAllDeepKeys = function(obj, keys, msg) { + new Assertion( + obj, + msg, + assert.containsAllDeepKeys, + true + ).to.contain.all.deep.keys(keys); +}; +assert.doesNotHaveAnyDeepKeys = function(obj, keys, msg) { + new Assertion( + obj, + msg, + assert.doesNotHaveAnyDeepKeys, + true + ).to.not.have.any.deep.keys(keys); +}; +assert.doesNotHaveAllDeepKeys = function(obj, keys, msg) { + new Assertion( + obj, + msg, + assert.doesNotHaveAllDeepKeys, + true + ).to.not.have.all.deep.keys(keys); +}; +assert.throws = function(fn, errorLike, errMsgMatcher, msg) { + if ("string" === typeof errorLike || errorLike instanceof RegExp) { + errMsgMatcher = errorLike; + errorLike = null; + } + let assertErr = new Assertion(fn, msg, assert.throws, true).to.throw( + errorLike, + errMsgMatcher + ); + return flag(assertErr, "object"); +}; +assert.doesNotThrow = function(fn, errorLike, errMsgMatcher, message) { + if ("string" === typeof errorLike || errorLike instanceof RegExp) { + errMsgMatcher = errorLike; + errorLike = null; + } + new Assertion(fn, message, assert.doesNotThrow, true).to.not.throw( + errorLike, + errMsgMatcher + ); +}; +assert.operator = function(val, operator, val2, msg) { + let ok; + switch (operator) { + case "==": + ok = val == val2; + break; + case "===": + ok = val === val2; + break; + case ">": + ok = val > val2; + break; + case ">=": + ok = val >= val2; + break; + case "<": + ok = val < val2; + break; + case "<=": + ok = val <= val2; + break; + case "!=": + ok = val != val2; + break; + case "!==": + ok = val !== val2; + break; + default: + msg = msg ? msg + ": " : msg; + throw new AssertionError( + msg + 'Invalid operator "' + operator + '"', + void 0, + assert.operator + ); + } + let test2 = new Assertion(ok, msg, assert.operator, true); + test2.assert( + true === flag(test2, "object"), + "expected " + inspect2(val) + " to be " + operator + " " + inspect2(val2), + "expected " + inspect2(val) + " to not be " + operator + " " + inspect2(val2) + ); +}; +assert.closeTo = function(act, exp, delta, msg) { + new Assertion(act, msg, assert.closeTo, true).to.be.closeTo(exp, delta); +}; +assert.approximately = function(act, exp, delta, msg) { + new Assertion(act, msg, assert.approximately, true).to.be.approximately( + exp, + delta + ); +}; +assert.sameMembers = function(set1, set2, msg) { + new Assertion(set1, msg, assert.sameMembers, true).to.have.same.members(set2); +}; +assert.notSameMembers = function(set1, set2, msg) { + new Assertion( + set1, + msg, + assert.notSameMembers, + true + ).to.not.have.same.members(set2); +}; +assert.sameDeepMembers = function(set1, set2, msg) { + new Assertion( + set1, + msg, + assert.sameDeepMembers, + true + ).to.have.same.deep.members(set2); +}; +assert.notSameDeepMembers = function(set1, set2, msg) { + new Assertion( + set1, + msg, + assert.notSameDeepMembers, + true + ).to.not.have.same.deep.members(set2); +}; +assert.sameOrderedMembers = function(set1, set2, msg) { + new Assertion( + set1, + msg, + assert.sameOrderedMembers, + true + ).to.have.same.ordered.members(set2); +}; +assert.notSameOrderedMembers = function(set1, set2, msg) { + new Assertion( + set1, + msg, + assert.notSameOrderedMembers, + true + ).to.not.have.same.ordered.members(set2); +}; +assert.sameDeepOrderedMembers = function(set1, set2, msg) { + new Assertion( + set1, + msg, + assert.sameDeepOrderedMembers, + true + ).to.have.same.deep.ordered.members(set2); +}; +assert.notSameDeepOrderedMembers = function(set1, set2, msg) { + new Assertion( + set1, + msg, + assert.notSameDeepOrderedMembers, + true + ).to.not.have.same.deep.ordered.members(set2); +}; +assert.includeMembers = function(superset, subset, msg) { + new Assertion(superset, msg, assert.includeMembers, true).to.include.members( + subset + ); +}; +assert.notIncludeMembers = function(superset, subset, msg) { + new Assertion( + superset, + msg, + assert.notIncludeMembers, + true + ).to.not.include.members(subset); +}; +assert.includeDeepMembers = function(superset, subset, msg) { + new Assertion( + superset, + msg, + assert.includeDeepMembers, + true + ).to.include.deep.members(subset); +}; +assert.notIncludeDeepMembers = function(superset, subset, msg) { + new Assertion( + superset, + msg, + assert.notIncludeDeepMembers, + true + ).to.not.include.deep.members(subset); +}; +assert.includeOrderedMembers = function(superset, subset, msg) { + new Assertion( + superset, + msg, + assert.includeOrderedMembers, + true + ).to.include.ordered.members(subset); +}; +assert.notIncludeOrderedMembers = function(superset, subset, msg) { + new Assertion( + superset, + msg, + assert.notIncludeOrderedMembers, + true + ).to.not.include.ordered.members(subset); +}; +assert.includeDeepOrderedMembers = function(superset, subset, msg) { + new Assertion( + superset, + msg, + assert.includeDeepOrderedMembers, + true + ).to.include.deep.ordered.members(subset); +}; +assert.notIncludeDeepOrderedMembers = function(superset, subset, msg) { + new Assertion( + superset, + msg, + assert.notIncludeDeepOrderedMembers, + true + ).to.not.include.deep.ordered.members(subset); +}; +assert.oneOf = function(inList, list, msg) { + new Assertion(inList, msg, assert.oneOf, true).to.be.oneOf(list); +}; +assert.isIterable = function(obj, msg) { + if (obj == void 0 || !obj[Symbol.iterator]) { + msg = msg ? `${msg} expected ${inspect2(obj)} to be an iterable` : `expected ${inspect2(obj)} to be an iterable`; + throw new AssertionError(msg, void 0, assert.isIterable); + } +}; +assert.changes = function(fn, obj, prop, msg) { + if (arguments.length === 3 && typeof obj === "function") { + msg = prop; + prop = null; + } + new Assertion(fn, msg, assert.changes, true).to.change(obj, prop); +}; +assert.changesBy = function(fn, obj, prop, delta, msg) { + if (arguments.length === 4 && typeof obj === "function") { + let tmpMsg = delta; + delta = prop; + msg = tmpMsg; + } else if (arguments.length === 3) { + delta = prop; + prop = null; + } + new Assertion(fn, msg, assert.changesBy, true).to.change(obj, prop).by(delta); +}; +assert.doesNotChange = function(fn, obj, prop, msg) { + if (arguments.length === 3 && typeof obj === "function") { + msg = prop; + prop = null; + } + return new Assertion(fn, msg, assert.doesNotChange, true).to.not.change( + obj, + prop + ); +}; +assert.changesButNotBy = function(fn, obj, prop, delta, msg) { + if (arguments.length === 4 && typeof obj === "function") { + let tmpMsg = delta; + delta = prop; + msg = tmpMsg; + } else if (arguments.length === 3) { + delta = prop; + prop = null; + } + new Assertion(fn, msg, assert.changesButNotBy, true).to.change(obj, prop).but.not.by(delta); +}; +assert.increases = function(fn, obj, prop, msg) { + if (arguments.length === 3 && typeof obj === "function") { + msg = prop; + prop = null; + } + return new Assertion(fn, msg, assert.increases, true).to.increase(obj, prop); +}; +assert.increasesBy = function(fn, obj, prop, delta, msg) { + if (arguments.length === 4 && typeof obj === "function") { + let tmpMsg = delta; + delta = prop; + msg = tmpMsg; + } else if (arguments.length === 3) { + delta = prop; + prop = null; + } + new Assertion(fn, msg, assert.increasesBy, true).to.increase(obj, prop).by(delta); +}; +assert.doesNotIncrease = function(fn, obj, prop, msg) { + if (arguments.length === 3 && typeof obj === "function") { + msg = prop; + prop = null; + } + return new Assertion(fn, msg, assert.doesNotIncrease, true).to.not.increase( + obj, + prop + ); +}; +assert.increasesButNotBy = function(fn, obj, prop, delta, msg) { + if (arguments.length === 4 && typeof obj === "function") { + let tmpMsg = delta; + delta = prop; + msg = tmpMsg; + } else if (arguments.length === 3) { + delta = prop; + prop = null; + } + new Assertion(fn, msg, assert.increasesButNotBy, true).to.increase(obj, prop).but.not.by(delta); +}; +assert.decreases = function(fn, obj, prop, msg) { + if (arguments.length === 3 && typeof obj === "function") { + msg = prop; + prop = null; + } + return new Assertion(fn, msg, assert.decreases, true).to.decrease(obj, prop); +}; +assert.decreasesBy = function(fn, obj, prop, delta, msg) { + if (arguments.length === 4 && typeof obj === "function") { + let tmpMsg = delta; + delta = prop; + msg = tmpMsg; + } else if (arguments.length === 3) { + delta = prop; + prop = null; + } + new Assertion(fn, msg, assert.decreasesBy, true).to.decrease(obj, prop).by(delta); +}; +assert.doesNotDecrease = function(fn, obj, prop, msg) { + if (arguments.length === 3 && typeof obj === "function") { + msg = prop; + prop = null; + } + return new Assertion(fn, msg, assert.doesNotDecrease, true).to.not.decrease( + obj, + prop + ); +}; +assert.doesNotDecreaseBy = function(fn, obj, prop, delta, msg) { + if (arguments.length === 4 && typeof obj === "function") { + let tmpMsg = delta; + delta = prop; + msg = tmpMsg; + } else if (arguments.length === 3) { + delta = prop; + prop = null; + } + return new Assertion(fn, msg, assert.doesNotDecreaseBy, true).to.not.decrease(obj, prop).by(delta); +}; +assert.decreasesButNotBy = function(fn, obj, prop, delta, msg) { + if (arguments.length === 4 && typeof obj === "function") { + let tmpMsg = delta; + delta = prop; + msg = tmpMsg; + } else if (arguments.length === 3) { + delta = prop; + prop = null; + } + new Assertion(fn, msg, assert.decreasesButNotBy, true).to.decrease(obj, prop).but.not.by(delta); +}; +assert.ifError = function(val) { + if (val) { + throw val; + } +}; +assert.isExtensible = function(obj, msg) { + new Assertion(obj, msg, assert.isExtensible, true).to.be.extensible; +}; +assert.isNotExtensible = function(obj, msg) { + new Assertion(obj, msg, assert.isNotExtensible, true).to.not.be.extensible; +}; +assert.isSealed = function(obj, msg) { + new Assertion(obj, msg, assert.isSealed, true).to.be.sealed; +}; +assert.isNotSealed = function(obj, msg) { + new Assertion(obj, msg, assert.isNotSealed, true).to.not.be.sealed; +}; +assert.isFrozen = function(obj, msg) { + new Assertion(obj, msg, assert.isFrozen, true).to.be.frozen; +}; +assert.isNotFrozen = function(obj, msg) { + new Assertion(obj, msg, assert.isNotFrozen, true).to.not.be.frozen; +}; +assert.isEmpty = function(val, msg) { + new Assertion(val, msg, assert.isEmpty, true).to.be.empty; +}; +assert.isNotEmpty = function(val, msg) { + new Assertion(val, msg, assert.isNotEmpty, true).to.not.be.empty; +}; +assert.containsSubset = function(val, exp, msg) { + new Assertion(val, msg).to.containSubset(exp); +}; +assert.doesNotContainSubset = function(val, exp, msg) { + new Assertion(val, msg).to.not.containSubset(exp); +}; +var aliases = [ + ["isOk", "ok"], + ["isNotOk", "notOk"], + ["throws", "throw"], + ["throws", "Throw"], + ["isExtensible", "extensible"], + ["isNotExtensible", "notExtensible"], + ["isSealed", "sealed"], + ["isNotSealed", "notSealed"], + ["isFrozen", "frozen"], + ["isNotFrozen", "notFrozen"], + ["isEmpty", "empty"], + ["isNotEmpty", "notEmpty"], + ["isCallable", "isFunction"], + ["isNotCallable", "isNotFunction"], + ["containsSubset", "containSubset"] +]; +for (const [name, as] of aliases) { + assert[as] = assert[name]; +} + +// lib/chai.js +var used = []; +function use(fn) { + const exports = { + use, + AssertionError, + util: utils_exports, + config, + expect, + assert, + Assertion, + ...should_exports + }; + if (!~used.indexOf(fn)) { + fn(exports, utils_exports); + used.push(fn); + } + return exports; +} +__name(use, "use"); +export { + Assertion, + AssertionError, + Should, + assert, + config, + expect, + should, + use, + utils_exports as util +}; +/*! + * Chai - flag utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - test utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - expectTypes utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - getActual utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - message composition utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - transferFlags utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * chai + * http://chaijs.com + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - isProxyEnabled helper + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - addProperty utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - addLengthGuard utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - getProperties utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - proxify utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - addMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - overwriteProperty utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - overwriteMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - addChainingMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - overwriteChainableMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ +/*! + * Chai - compareByInspect utility + * Copyright(c) 2011-2016 Jake Luer + * MIT Licensed + */ +/*! + * Chai - getOwnEnumerablePropertySymbols utility + * Copyright(c) 2011-2016 Jake Luer + * MIT Licensed + */ +/*! + * Chai - getOwnEnumerableProperties utility + * Copyright(c) 2011-2016 Jake Luer + * MIT Licensed + */ +/*! + * Chai - isNaN utility + * Copyright(c) 2012-2015 Sakthipriyan Vairamani + * MIT Licensed + */ +/*! + * chai + * Copyright(c) 2011 Jake Luer + * MIT Licensed + */ +/*! + * chai + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ +/*! Bundled license information: + +deep-eql/index.js: + (*! + * deep-eql + * Copyright(c) 2013 Jake Luer + * MIT Licensed + *) + (*! + * Check to see if the MemoizeMap has recorded a result of the two operands + * + * @param {Mixed} leftHandOperand + * @param {Mixed} rightHandOperand + * @param {MemoizeMap} memoizeMap + * @returns {Boolean|null} result + *) + (*! + * Set the result of the equality into the MemoizeMap + * + * @param {Mixed} leftHandOperand + * @param {Mixed} rightHandOperand + * @param {MemoizeMap} memoizeMap + * @param {Boolean} result + *) + (*! + * Primary Export + *) + (*! + * The main logic of the `deepEqual` function. + * + * @param {Mixed} leftHandOperand + * @param {Mixed} rightHandOperand + * @param {Object} [options] (optional) Additional options + * @param {Array} [options.comparator] (optional) Override default algorithm, determining custom equality. + * @param {Array} [options.memoize] (optional) Provide a custom memoization object which will cache the results of + complex objects for a speed boost. By passing `false` you can disable memoization, but this will cause circular + references to blow the stack. + * @return {Boolean} equal match + *) + (*! + * Compare two Regular Expressions for equality. + * + * @param {RegExp} leftHandOperand + * @param {RegExp} rightHandOperand + * @return {Boolean} result + *) + (*! + * Compare two Sets/Maps for equality. Faster than other equality functions. + * + * @param {Set} leftHandOperand + * @param {Set} rightHandOperand + * @param {Object} [options] (Optional) + * @return {Boolean} result + *) + (*! + * Simple equality for flat iterable objects such as Arrays, TypedArrays or Node.js buffers. + * + * @param {Iterable} leftHandOperand + * @param {Iterable} rightHandOperand + * @param {Object} [options] (Optional) + * @return {Boolean} result + *) + (*! + * Simple equality for generator objects such as those returned by generator functions. + * + * @param {Iterable} leftHandOperand + * @param {Iterable} rightHandOperand + * @param {Object} [options] (Optional) + * @return {Boolean} result + *) + (*! + * Determine if the given object has an @@iterator function. + * + * @param {Object} target + * @return {Boolean} `true` if the object has an @@iterator function. + *) + (*! + * Gets all iterator entries from the given Object. If the Object has no @@iterator function, returns an empty array. + * This will consume the iterator - which could have side effects depending on the @@iterator implementation. + * + * @param {Object} target + * @returns {Array} an array of entries from the @@iterator function + *) + (*! + * Gets all entries from a Generator. This will consume the generator - which could have side effects. + * + * @param {Generator} target + * @returns {Array} an array of entries from the Generator. + *) + (*! + * Gets all own and inherited enumerable keys from a target. + * + * @param {Object} target + * @returns {Array} an array of own and inherited enumerable keys from the target. + *) + (*! + * Determines if two objects have matching values, given a set of keys. Defers to deepEqual for the equality check of + * each key. If any value of the given key is not equal, the function will return false (early). + * + * @param {Mixed} leftHandOperand + * @param {Mixed} rightHandOperand + * @param {Array} keys An array of keys to compare the values of leftHandOperand and rightHandOperand against + * @param {Object} [options] (Optional) + * @return {Boolean} result + *) + (*! + * Recursively check the equality of two Objects. Once basic sameness has been established it will defer to `deepEqual` + * for each enumerable key in the object. + * + * @param {Mixed} leftHandOperand + * @param {Mixed} rightHandOperand + * @param {Object} [options] (Optional) + * @return {Boolean} result + *) + (*! + * Returns true if the argument is a primitive. + * + * This intentionally returns true for all objects that can be compared by reference, + * including functions and symbols. + * + * @param {Mixed} value + * @return {Boolean} result + *) +*/ diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/package.json new file mode 100644 index 00000000..872a8dc3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/package.json @@ -0,0 +1,74 @@ +{ + "author": "Jake Luer ", + "name": "chai", + "type": "module", + "description": "BDD/TDD assertion library for node.js and the browser. Test framework agnostic.", + "keywords": [ + "test", + "assertion", + "assert", + "testing", + "chai" + ], + "files": [ + "chai.js", + "index.js", + "lib", + "register-*.js" + ], + "homepage": "http://chaijs.com", + "license": "MIT", + "contributors": [ + "Jake Luer ", + "Domenic Denicola (http://domenicdenicola.com)", + "Veselin Todorov ", + "John Firebaugh " + ], + "version": "5.3.3", + "repository": { + "type": "git", + "url": "https://github.com/chaijs/chai" + }, + "bugs": { + "url": "https://github.com/chaijs/chai/issues" + }, + "main": "./index.js", + "scripts": { + "build": "esbuild --bundle --format=esm --keep-names --outfile=index.js lib/chai.js", + "prebuild": "npm run clean", + "format": "prettier --write lib", + "pretest": "npm run lint", + "test": "npm run test-node && npm run test-chrome", + "test-node": "c8 --99 --check-coverage mocha --require ./test/bootstrap/index.js test/*.js", + "test-chrome": "web-test-runner --playwright", + "lint": "npm run lint:js && npm run lint:format", + "lint:js": "eslint lib/", + "lint:format": "prettier --check lib", + "lint:types": "tsc", + "clean": "rm -rf index.js coverage/" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@rollup/plugin-commonjs": "^25.0.7", + "@web/dev-server-rollup": "^0.6.1", + "@web/test-runner": "^0.18.0", + "@web/test-runner-playwright": "^0.11.0", + "c8": "^10.1.3", + "esbuild": "^0.25.9", + "eslint": "^8.56.0", + "eslint-plugin-jsdoc": "^48.0.4", + "mocha": "^10.2.0", + "prettier": "^3.4.2", + "typescript": "~5.7.3" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-assert.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-assert.js new file mode 100644 index 00000000..f593717e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-assert.js @@ -0,0 +1,3 @@ +import {assert} from './index.js'; + +globalThis.assert = assert; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-expect.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-expect.js new file mode 100644 index 00000000..2807b89b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-expect.js @@ -0,0 +1,3 @@ +import {expect} from './index.js'; + +globalThis.expect = expect; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-should.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-should.js new file mode 100644 index 00000000..1339ee4c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/chai/register-should.js @@ -0,0 +1,3 @@ +import {should} from './index.js'; + +globalThis.should = should(); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/LICENSE new file mode 100644 index 00000000..7ea799f0 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/README.md new file mode 100644 index 00000000..d1c19497 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/README.md @@ -0,0 +1,144 @@ +

+ + ChaiJS + +
+ check-error +

+ +

+ Error comparison and information related utility for node and the browser. +

+ +## What is Check-Error? + +Check-Error is a module which you can use to retrieve an Error's information such as its `message` or `constructor` name and also to check whether two Errors are compatible based on their messages, constructors or even instances. + +## Installation + +### Node.js + +`check-error` is available on [npm](http://npmjs.org). To install it, type: + + $ npm install check-error + +### Browsers + +You can also use it within the browser; install via npm and use the `check-error.js` file found within the download. For example: + +```html + +``` + +## Usage + +The primary export of `check-error` is an object which has the following methods: + +* `compatibleInstance(err, errorLike)` - Checks if an error is compatible with another `errorLike` object. If `errorLike` is an error instance we do a strict comparison, otherwise we return `false` by default, because instances of objects can only be compatible if they're both error instances. +* `compatibleConstructor(err, errorLike)` - Checks if an error's constructor is compatible with another `errorLike` object. If `err` has the same constructor as `errorLike` or if `err` is an instance of `errorLike`. +* `compatibleMessage(err, errMatcher)` - Checks if an error message is compatible with an `errMatcher` RegExp or String (we check if the message contains the String). +* `getConstructorName(errorLike)` - Retrieves the name of a constructor, an error's constructor or `errorLike` itself if it's not an error instance or constructor. +* `getMessage(err)` - Retrieves the message of an error or `err` itself if it's a String. If `err` or `err.message` is undefined we return an empty String. + +```js +import * as checkError 'check-error'; +``` + +#### .compatibleInstance(err, errorLike) + +```js +import * as checkError 'check-error'; + +const funcThatThrows = function() { throw new TypeError('I am a TypeError') }; +let caughtErr; + +try { + funcThatThrows(); +} catch(e) { + caughtErr = e; +} + +const sameInstance = caughtErr; + +checkError.compatibleInstance(caughtErr, sameInstance); // true +checkError.compatibleInstance(caughtErr, new TypeError('Another error')); // false +``` + +#### .compatibleConstructor(err, errorLike) + +```js +import * as checkError 'check-error'; + +const funcThatThrows = function() { throw new TypeError('I am a TypeError') }; +let caughtErr; + +try { + funcThatThrows(); +} catch(e) { + caughtErr = e; +} + +checkError.compatibleConstructor(caughtErr, Error); // true +checkError.compatibleConstructor(caughtErr, TypeError); // true +checkError.compatibleConstructor(caughtErr, RangeError); // false +``` + +#### .compatibleMessage(err, errMatcher) + +```js +import * as checkError 'check-error'; + +const funcThatThrows = function() { throw new TypeError('I am a TypeError') }; +let caughtErr; + +try { + funcThatThrows(); +} catch(e) { + caughtErr = e; +} + +const sameInstance = caughtErr; + +checkError.compatibleMessage(caughtErr, /TypeError$/); // true +checkError.compatibleMessage(caughtErr, 'I am a'); // true +checkError.compatibleMessage(caughtErr, /unicorn/); // false +checkError.compatibleMessage(caughtErr, 'I do not exist'); // false +``` + +#### .getConstructorName(errorLike) + +```js +import * as checkError 'check-error'; + +const funcThatThrows = function() { throw new TypeError('I am a TypeError') }; +let caughtErr; + +try { + funcThatThrows(); +} catch(e) { + caughtErr = e; +} + +const sameInstance = caughtErr; + +checkError.getConstructorName(caughtErr) // 'TypeError' +``` + +#### .getMessage(err) + +```js +import * as checkError 'check-error'; + +const funcThatThrows = function() { throw new TypeError('I am a TypeError') }; +let caughtErr; + +try { + funcThatThrows(); +} catch(e) { + caughtErr = e; +} + +const sameInstance = caughtErr; + +checkError.getMessage(caughtErr) // 'I am a TypeError' +``` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/index.js new file mode 100644 index 00000000..ee5450f9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/index.js @@ -0,0 +1,136 @@ +function isErrorInstance(obj) { + return obj instanceof Error || Object.prototype.toString.call(obj) === '[object Error]'; +} + +function isRegExp(obj) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +/** + * ### .compatibleInstance(thrown, errorLike) + * + * Checks if two instances are compatible (strict equal). + * Returns false if errorLike is not an instance of Error, because instances + * can only be compatible if they're both error instances. + * + * @name compatibleInstance + * @param {Error} thrown error + * @param {Error|ErrorConstructor} errorLike object to compare against + * @namespace Utils + * @api public + */ + +function compatibleInstance(thrown, errorLike) { + return isErrorInstance(errorLike) && thrown === errorLike; +} + +/** + * ### .compatibleConstructor(thrown, errorLike) + * + * Checks if two constructors are compatible. + * This function can receive either an error constructor or + * an error instance as the `errorLike` argument. + * Constructors are compatible if they're the same or if one is + * an instance of another. + * + * @name compatibleConstructor + * @param {Error} thrown error + * @param {Error|ErrorConstructor} errorLike object to compare against + * @namespace Utils + * @api public + */ + +function compatibleConstructor(thrown, errorLike) { + if (isErrorInstance(errorLike)) { + // If `errorLike` is an instance of any error we compare their constructors + return thrown.constructor === errorLike.constructor || thrown instanceof errorLike.constructor; + } else if ((typeof errorLike === 'object' || typeof errorLike === 'function') && errorLike.prototype) { + // If `errorLike` is a constructor that inherits from Error, we compare `thrown` to `errorLike` directly + return thrown.constructor === errorLike || thrown instanceof errorLike; + } + + return false; +} + +/** + * ### .compatibleMessage(thrown, errMatcher) + * + * Checks if an error's message is compatible with a matcher (String or RegExp). + * If the message contains the String or passes the RegExp test, + * it is considered compatible. + * + * @name compatibleMessage + * @param {Error} thrown error + * @param {String|RegExp} errMatcher to look for into the message + * @namespace Utils + * @api public + */ + +function compatibleMessage(thrown, errMatcher) { + const comparisonString = typeof thrown === 'string' ? thrown : thrown.message; + if (comparisonString === undefined) { + return false; + } + if (isRegExp(errMatcher)) { + return errMatcher.test(comparisonString); + } else if (typeof errMatcher === 'string') { + return comparisonString.indexOf(errMatcher) !== -1; + } + + return false; +} + +/** + * ### .getConstructorName(errorLike) + * + * Gets the constructor name for an Error instance or constructor itself. + * + * @name getConstructorName + * @param {Error|ErrorConstructor} errorLike + * @namespace Utils + * @api public + */ + +function getConstructorName(errorLike) { + let constructorName = errorLike; + if (isErrorInstance(errorLike)) { + constructorName = errorLike.constructor.name; + } else if (typeof errorLike === 'function') { + // If `err` is not an instance of Error it is an error constructor itself or another function. + // If we've got a common function we get its name, otherwise we may need to create a new instance + // of the error just in case it's a poorly-constructed error. Please see chaijs/chai/issues/45 to know more. + constructorName = errorLike.name; + if (constructorName === '') { + const newConstructorName = (new errorLike().name); + constructorName = newConstructorName || constructorName; + } + } + + return constructorName; +} + +/** + * ### .getMessage(errorLike) + * + * Gets the error message from an error. + * If `err` is a String itself, we return it. + * If the error has no message, we return an empty string. + * + * @name getMessage + * @param {Error|String} errorLike + * @namespace Utils + * @api public + */ + +function getMessage(errorLike) { + let msg = ''; + if (errorLike && errorLike.message) { + msg = errorLike.message; + } else if (typeof errorLike === 'string') { + msg = errorLike; + } + + return msg; +} + +export { compatibleInstance, compatibleConstructor, compatibleMessage, getMessage, getConstructorName }; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/package.json new file mode 100644 index 00000000..efbaaf73 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/check-error/package.json @@ -0,0 +1,46 @@ +{ + "name": "check-error", + "description": "Error comparison and information related utility for node and the browser", + "keywords": [ + "check-error", + "error", + "chai util" + ], + "license": "MIT", + "author": "Jake Luer (http://alogicalparadox.com)", + "contributors": [ + "David Losert (https://github.com/davelosert)", + "Keith Cirkel (https://github.com/keithamus)", + "Miroslav Bajtoš (https://github.com/bajtos)", + "Lucas Fernandes da Costa (https://github.com/lucasfcosta)" + ], + "files": [ + "index.js", + "check-error.js" + ], + "type": "module", + "main": "./index.js", + "module": "./index.js", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/chaijs/check-error.git" + }, + "scripts": { + "lint": "eslint index.js test/", + "pretest": "npm run lint", + "test": "npm run test:node && npm run test:browser", + "test:browser": "web-test-runner", + "test:node": "mocha" + }, + "devDependencies": { + "@eslint/js": "^9.31.0", + "@web/test-runner": "^0.20.2", + "eslint": "^9.31.0", + "mocha": "^11.7.1", + "simple-assert": "^2.0.0" + }, + "engines": { + "node": ">= 16" + }, + "version": "2.1.3" +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/CHANGELOG.md new file mode 100644 index 00000000..0a7bce4f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/CHANGELOG.md @@ -0,0 +1,54 @@ +# 1.0.0 - 2016-01-07 + +- Removed: unused speed test +- Added: Automatic routing between previously unsupported conversions +([#27](https://github.com/Qix-/color-convert/pull/27)) +- Removed: `xxx2xxx()` and `xxx2xxxRaw()` functions +([#27](https://github.com/Qix-/color-convert/pull/27)) +- Removed: `convert()` class +([#27](https://github.com/Qix-/color-convert/pull/27)) +- Changed: all functions to lookup dictionary +([#27](https://github.com/Qix-/color-convert/pull/27)) +- Changed: `ansi` to `ansi256` +([#27](https://github.com/Qix-/color-convert/pull/27)) +- Fixed: argument grouping for functions requiring only one argument +([#27](https://github.com/Qix-/color-convert/pull/27)) + +# 0.6.0 - 2015-07-23 + +- Added: methods to handle +[ANSI](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) 16/256 colors: + - rgb2ansi16 + - rgb2ansi + - hsl2ansi16 + - hsl2ansi + - hsv2ansi16 + - hsv2ansi + - hwb2ansi16 + - hwb2ansi + - cmyk2ansi16 + - cmyk2ansi + - keyword2ansi16 + - keyword2ansi + - ansi162rgb + - ansi162hsl + - ansi162hsv + - ansi162hwb + - ansi162cmyk + - ansi162keyword + - ansi2rgb + - ansi2hsl + - ansi2hsv + - ansi2hwb + - ansi2cmyk + - ansi2keyword +([#18](https://github.com/harthur/color-convert/pull/18)) + +# 0.5.3 - 2015-06-02 + +- Fixed: hsl2hsv does not return `NaN` anymore when using `[0,0,0]` +([#15](https://github.com/harthur/color-convert/issues/15)) + +--- + +Check out commit logs for older releases diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/LICENSE new file mode 100644 index 00000000..5b4c386f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/README.md new file mode 100644 index 00000000..d4b08fc3 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/README.md @@ -0,0 +1,68 @@ +# color-convert + +[![Build Status](https://travis-ci.org/Qix-/color-convert.svg?branch=master)](https://travis-ci.org/Qix-/color-convert) + +Color-convert is a color conversion library for JavaScript and node. +It converts all ways between `rgb`, `hsl`, `hsv`, `hwb`, `cmyk`, `ansi`, `ansi16`, `hex` strings, and CSS `keyword`s (will round to closest): + +```js +var convert = require('color-convert'); + +convert.rgb.hsl(140, 200, 100); // [96, 48, 59] +convert.keyword.rgb('blue'); // [0, 0, 255] + +var rgbChannels = convert.rgb.channels; // 3 +var cmykChannels = convert.cmyk.channels; // 4 +var ansiChannels = convert.ansi16.channels; // 1 +``` + +# Install + +```console +$ npm install color-convert +``` + +# API + +Simply get the property of the _from_ and _to_ conversion that you're looking for. + +All functions have a rounded and unrounded variant. By default, return values are rounded. To get the unrounded (raw) results, simply tack on `.raw` to the function. + +All 'from' functions have a hidden property called `.channels` that indicates the number of channels the function expects (not including alpha). + +```js +var convert = require('color-convert'); + +// Hex to LAB +convert.hex.lab('DEADBF'); // [ 76, 21, -2 ] +convert.hex.lab.raw('DEADBF'); // [ 75.56213190997677, 20.653827952644754, -2.290532499330533 ] + +// RGB to CMYK +convert.rgb.cmyk(167, 255, 4); // [ 35, 0, 98, 0 ] +convert.rgb.cmyk.raw(167, 255, 4); // [ 34.509803921568626, 0, 98.43137254901961, 0 ] +``` + +### Arrays +All functions that accept multiple arguments also support passing an array. + +Note that this does **not** apply to functions that convert from a color that only requires one value (e.g. `keyword`, `ansi256`, `hex`, etc.) + +```js +var convert = require('color-convert'); + +convert.rgb.hex(123, 45, 67); // '7B2D43' +convert.rgb.hex([123, 45, 67]); // '7B2D43' +``` + +## Routing + +Conversions that don't have an _explicitly_ defined conversion (in [conversions.js](conversions.js)), but can be converted by means of sub-conversions (e.g. XYZ -> **RGB** -> CMYK), are automatically routed together. This allows just about any color model supported by `color-convert` to be converted to any other model, so long as a sub-conversion path exists. This is also true for conversions requiring more than one step in between (e.g. LCH -> **LAB** -> **XYZ** -> **RGB** -> Hex). + +Keep in mind that extensive conversions _may_ result in a loss of precision, and exist only to be complete. For a list of "direct" (single-step) conversions, see [conversions.js](conversions.js). + +# Contribute + +If there is a new model you would like to support, or want to add a direct conversion between two existing models, please send us a pull request. + +# License +Copyright © 2011-2016, Heather Arthur and Josh Junon. Licensed under the [MIT License](LICENSE). diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/conversions.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/conversions.js new file mode 100644 index 00000000..2657f265 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/conversions.js @@ -0,0 +1,839 @@ +/* MIT license */ +/* eslint-disable no-mixed-operators */ +const cssKeywords = require('color-name'); + +// NOTE: conversions should only return primitive values (i.e. arrays, or +// values that give correct `typeof` results). +// do not use box values types (i.e. Number(), String(), etc.) + +const reverseKeywords = {}; +for (const key of Object.keys(cssKeywords)) { + reverseKeywords[cssKeywords[key]] = key; +} + +const convert = { + rgb: {channels: 3, labels: 'rgb'}, + hsl: {channels: 3, labels: 'hsl'}, + hsv: {channels: 3, labels: 'hsv'}, + hwb: {channels: 3, labels: 'hwb'}, + cmyk: {channels: 4, labels: 'cmyk'}, + xyz: {channels: 3, labels: 'xyz'}, + lab: {channels: 3, labels: 'lab'}, + lch: {channels: 3, labels: 'lch'}, + hex: {channels: 1, labels: ['hex']}, + keyword: {channels: 1, labels: ['keyword']}, + ansi16: {channels: 1, labels: ['ansi16']}, + ansi256: {channels: 1, labels: ['ansi256']}, + hcg: {channels: 3, labels: ['h', 'c', 'g']}, + apple: {channels: 3, labels: ['r16', 'g16', 'b16']}, + gray: {channels: 1, labels: ['gray']} +}; + +module.exports = convert; + +// Hide .channels and .labels properties +for (const model of Object.keys(convert)) { + if (!('channels' in convert[model])) { + throw new Error('missing channels property: ' + model); + } + + if (!('labels' in convert[model])) { + throw new Error('missing channel labels property: ' + model); + } + + if (convert[model].labels.length !== convert[model].channels) { + throw new Error('channel and label counts mismatch: ' + model); + } + + const {channels, labels} = convert[model]; + delete convert[model].channels; + delete convert[model].labels; + Object.defineProperty(convert[model], 'channels', {value: channels}); + Object.defineProperty(convert[model], 'labels', {value: labels}); +} + +convert.rgb.hsl = function (rgb) { + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const min = Math.min(r, g, b); + const max = Math.max(r, g, b); + const delta = max - min; + let h; + let s; + + if (max === min) { + h = 0; + } else if (r === max) { + h = (g - b) / delta; + } else if (g === max) { + h = 2 + (b - r) / delta; + } else if (b === max) { + h = 4 + (r - g) / delta; + } + + h = Math.min(h * 60, 360); + + if (h < 0) { + h += 360; + } + + const l = (min + max) / 2; + + if (max === min) { + s = 0; + } else if (l <= 0.5) { + s = delta / (max + min); + } else { + s = delta / (2 - max - min); + } + + return [h, s * 100, l * 100]; +}; + +convert.rgb.hsv = function (rgb) { + let rdif; + let gdif; + let bdif; + let h; + let s; + + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const v = Math.max(r, g, b); + const diff = v - Math.min(r, g, b); + const diffc = function (c) { + return (v - c) / 6 / diff + 1 / 2; + }; + + if (diff === 0) { + h = 0; + s = 0; + } else { + s = diff / v; + rdif = diffc(r); + gdif = diffc(g); + bdif = diffc(b); + + if (r === v) { + h = bdif - gdif; + } else if (g === v) { + h = (1 / 3) + rdif - bdif; + } else if (b === v) { + h = (2 / 3) + gdif - rdif; + } + + if (h < 0) { + h += 1; + } else if (h > 1) { + h -= 1; + } + } + + return [ + h * 360, + s * 100, + v * 100 + ]; +}; + +convert.rgb.hwb = function (rgb) { + const r = rgb[0]; + const g = rgb[1]; + let b = rgb[2]; + const h = convert.rgb.hsl(rgb)[0]; + const w = 1 / 255 * Math.min(r, Math.min(g, b)); + + b = 1 - 1 / 255 * Math.max(r, Math.max(g, b)); + + return [h, w * 100, b * 100]; +}; + +convert.rgb.cmyk = function (rgb) { + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + + const k = Math.min(1 - r, 1 - g, 1 - b); + const c = (1 - r - k) / (1 - k) || 0; + const m = (1 - g - k) / (1 - k) || 0; + const y = (1 - b - k) / (1 - k) || 0; + + return [c * 100, m * 100, y * 100, k * 100]; +}; + +function comparativeDistance(x, y) { + /* + See https://en.m.wikipedia.org/wiki/Euclidean_distance#Squared_Euclidean_distance + */ + return ( + ((x[0] - y[0]) ** 2) + + ((x[1] - y[1]) ** 2) + + ((x[2] - y[2]) ** 2) + ); +} + +convert.rgb.keyword = function (rgb) { + const reversed = reverseKeywords[rgb]; + if (reversed) { + return reversed; + } + + let currentClosestDistance = Infinity; + let currentClosestKeyword; + + for (const keyword of Object.keys(cssKeywords)) { + const value = cssKeywords[keyword]; + + // Compute comparative distance + const distance = comparativeDistance(rgb, value); + + // Check if its less, if so set as closest + if (distance < currentClosestDistance) { + currentClosestDistance = distance; + currentClosestKeyword = keyword; + } + } + + return currentClosestKeyword; +}; + +convert.keyword.rgb = function (keyword) { + return cssKeywords[keyword]; +}; + +convert.rgb.xyz = function (rgb) { + let r = rgb[0] / 255; + let g = rgb[1] / 255; + let b = rgb[2] / 255; + + // Assume sRGB + r = r > 0.04045 ? (((r + 0.055) / 1.055) ** 2.4) : (r / 12.92); + g = g > 0.04045 ? (((g + 0.055) / 1.055) ** 2.4) : (g / 12.92); + b = b > 0.04045 ? (((b + 0.055) / 1.055) ** 2.4) : (b / 12.92); + + const x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); + const y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); + const z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); + + return [x * 100, y * 100, z * 100]; +}; + +convert.rgb.lab = function (rgb) { + const xyz = convert.rgb.xyz(rgb); + let x = xyz[0]; + let y = xyz[1]; + let z = xyz[2]; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? (x ** (1 / 3)) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? (y ** (1 / 3)) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? (z ** (1 / 3)) : (7.787 * z) + (16 / 116); + + const l = (116 * y) - 16; + const a = 500 * (x - y); + const b = 200 * (y - z); + + return [l, a, b]; +}; + +convert.hsl.rgb = function (hsl) { + const h = hsl[0] / 360; + const s = hsl[1] / 100; + const l = hsl[2] / 100; + let t2; + let t3; + let val; + + if (s === 0) { + val = l * 255; + return [val, val, val]; + } + + if (l < 0.5) { + t2 = l * (1 + s); + } else { + t2 = l + s - l * s; + } + + const t1 = 2 * l - t2; + + const rgb = [0, 0, 0]; + for (let i = 0; i < 3; i++) { + t3 = h + 1 / 3 * -(i - 1); + if (t3 < 0) { + t3++; + } + + if (t3 > 1) { + t3--; + } + + if (6 * t3 < 1) { + val = t1 + (t2 - t1) * 6 * t3; + } else if (2 * t3 < 1) { + val = t2; + } else if (3 * t3 < 2) { + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + } else { + val = t1; + } + + rgb[i] = val * 255; + } + + return rgb; +}; + +convert.hsl.hsv = function (hsl) { + const h = hsl[0]; + let s = hsl[1] / 100; + let l = hsl[2] / 100; + let smin = s; + const lmin = Math.max(l, 0.01); + + l *= 2; + s *= (l <= 1) ? l : 2 - l; + smin *= lmin <= 1 ? lmin : 2 - lmin; + const v = (l + s) / 2; + const sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s); + + return [h, sv * 100, v * 100]; +}; + +convert.hsv.rgb = function (hsv) { + const h = hsv[0] / 60; + const s = hsv[1] / 100; + let v = hsv[2] / 100; + const hi = Math.floor(h) % 6; + + const f = h - Math.floor(h); + const p = 255 * v * (1 - s); + const q = 255 * v * (1 - (s * f)); + const t = 255 * v * (1 - (s * (1 - f))); + v *= 255; + + switch (hi) { + case 0: + return [v, t, p]; + case 1: + return [q, v, p]; + case 2: + return [p, v, t]; + case 3: + return [p, q, v]; + case 4: + return [t, p, v]; + case 5: + return [v, p, q]; + } +}; + +convert.hsv.hsl = function (hsv) { + const h = hsv[0]; + const s = hsv[1] / 100; + const v = hsv[2] / 100; + const vmin = Math.max(v, 0.01); + let sl; + let l; + + l = (2 - s) * v; + const lmin = (2 - s) * vmin; + sl = s * vmin; + sl /= (lmin <= 1) ? lmin : 2 - lmin; + sl = sl || 0; + l /= 2; + + return [h, sl * 100, l * 100]; +}; + +// http://dev.w3.org/csswg/css-color/#hwb-to-rgb +convert.hwb.rgb = function (hwb) { + const h = hwb[0] / 360; + let wh = hwb[1] / 100; + let bl = hwb[2] / 100; + const ratio = wh + bl; + let f; + + // Wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } + + const i = Math.floor(6 * h); + const v = 1 - bl; + f = 6 * h - i; + + if ((i & 0x01) !== 0) { + f = 1 - f; + } + + const n = wh + f * (v - wh); // Linear interpolation + + let r; + let g; + let b; + /* eslint-disable max-statements-per-line,no-multi-spaces */ + switch (i) { + default: + case 6: + case 0: r = v; g = n; b = wh; break; + case 1: r = n; g = v; b = wh; break; + case 2: r = wh; g = v; b = n; break; + case 3: r = wh; g = n; b = v; break; + case 4: r = n; g = wh; b = v; break; + case 5: r = v; g = wh; b = n; break; + } + /* eslint-enable max-statements-per-line,no-multi-spaces */ + + return [r * 255, g * 255, b * 255]; +}; + +convert.cmyk.rgb = function (cmyk) { + const c = cmyk[0] / 100; + const m = cmyk[1] / 100; + const y = cmyk[2] / 100; + const k = cmyk[3] / 100; + + const r = 1 - Math.min(1, c * (1 - k) + k); + const g = 1 - Math.min(1, m * (1 - k) + k); + const b = 1 - Math.min(1, y * (1 - k) + k); + + return [r * 255, g * 255, b * 255]; +}; + +convert.xyz.rgb = function (xyz) { + const x = xyz[0] / 100; + const y = xyz[1] / 100; + const z = xyz[2] / 100; + let r; + let g; + let b; + + r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); + g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); + b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); + + // Assume sRGB + r = r > 0.0031308 + ? ((1.055 * (r ** (1.0 / 2.4))) - 0.055) + : r * 12.92; + + g = g > 0.0031308 + ? ((1.055 * (g ** (1.0 / 2.4))) - 0.055) + : g * 12.92; + + b = b > 0.0031308 + ? ((1.055 * (b ** (1.0 / 2.4))) - 0.055) + : b * 12.92; + + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); + + return [r * 255, g * 255, b * 255]; +}; + +convert.xyz.lab = function (xyz) { + let x = xyz[0]; + let y = xyz[1]; + let z = xyz[2]; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? (x ** (1 / 3)) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? (y ** (1 / 3)) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? (z ** (1 / 3)) : (7.787 * z) + (16 / 116); + + const l = (116 * y) - 16; + const a = 500 * (x - y); + const b = 200 * (y - z); + + return [l, a, b]; +}; + +convert.lab.xyz = function (lab) { + const l = lab[0]; + const a = lab[1]; + const b = lab[2]; + let x; + let y; + let z; + + y = (l + 16) / 116; + x = a / 500 + y; + z = y - b / 200; + + const y2 = y ** 3; + const x2 = x ** 3; + const z2 = z ** 3; + y = y2 > 0.008856 ? y2 : (y - 16 / 116) / 7.787; + x = x2 > 0.008856 ? x2 : (x - 16 / 116) / 7.787; + z = z2 > 0.008856 ? z2 : (z - 16 / 116) / 7.787; + + x *= 95.047; + y *= 100; + z *= 108.883; + + return [x, y, z]; +}; + +convert.lab.lch = function (lab) { + const l = lab[0]; + const a = lab[1]; + const b = lab[2]; + let h; + + const hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; + + if (h < 0) { + h += 360; + } + + const c = Math.sqrt(a * a + b * b); + + return [l, c, h]; +}; + +convert.lch.lab = function (lch) { + const l = lch[0]; + const c = lch[1]; + const h = lch[2]; + + const hr = h / 360 * 2 * Math.PI; + const a = c * Math.cos(hr); + const b = c * Math.sin(hr); + + return [l, a, b]; +}; + +convert.rgb.ansi16 = function (args, saturation = null) { + const [r, g, b] = args; + let value = saturation === null ? convert.rgb.hsv(args)[2] : saturation; // Hsv -> ansi16 optimization + + value = Math.round(value / 50); + + if (value === 0) { + return 30; + } + + let ansi = 30 + + ((Math.round(b / 255) << 2) + | (Math.round(g / 255) << 1) + | Math.round(r / 255)); + + if (value === 2) { + ansi += 60; + } + + return ansi; +}; + +convert.hsv.ansi16 = function (args) { + // Optimization here; we already know the value and don't need to get + // it converted for us. + return convert.rgb.ansi16(convert.hsv.rgb(args), args[2]); +}; + +convert.rgb.ansi256 = function (args) { + const r = args[0]; + const g = args[1]; + const b = args[2]; + + // We use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (r === g && g === b) { + if (r < 8) { + return 16; + } + + if (r > 248) { + return 231; + } + + return Math.round(((r - 8) / 247) * 24) + 232; + } + + const ansi = 16 + + (36 * Math.round(r / 255 * 5)) + + (6 * Math.round(g / 255 * 5)) + + Math.round(b / 255 * 5); + + return ansi; +}; + +convert.ansi16.rgb = function (args) { + let color = args % 10; + + // Handle greyscale + if (color === 0 || color === 7) { + if (args > 50) { + color += 3.5; + } + + color = color / 10.5 * 255; + + return [color, color, color]; + } + + const mult = (~~(args > 50) + 1) * 0.5; + const r = ((color & 1) * mult) * 255; + const g = (((color >> 1) & 1) * mult) * 255; + const b = (((color >> 2) & 1) * mult) * 255; + + return [r, g, b]; +}; + +convert.ansi256.rgb = function (args) { + // Handle greyscale + if (args >= 232) { + const c = (args - 232) * 10 + 8; + return [c, c, c]; + } + + args -= 16; + + let rem; + const r = Math.floor(args / 36) / 5 * 255; + const g = Math.floor((rem = args % 36) / 6) / 5 * 255; + const b = (rem % 6) / 5 * 255; + + return [r, g, b]; +}; + +convert.rgb.hex = function (args) { + const integer = ((Math.round(args[0]) & 0xFF) << 16) + + ((Math.round(args[1]) & 0xFF) << 8) + + (Math.round(args[2]) & 0xFF); + + const string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; +}; + +convert.hex.rgb = function (args) { + const match = args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i); + if (!match) { + return [0, 0, 0]; + } + + let colorString = match[0]; + + if (match[0].length === 3) { + colorString = colorString.split('').map(char => { + return char + char; + }).join(''); + } + + const integer = parseInt(colorString, 16); + const r = (integer >> 16) & 0xFF; + const g = (integer >> 8) & 0xFF; + const b = integer & 0xFF; + + return [r, g, b]; +}; + +convert.rgb.hcg = function (rgb) { + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const max = Math.max(Math.max(r, g), b); + const min = Math.min(Math.min(r, g), b); + const chroma = (max - min); + let grayscale; + let hue; + + if (chroma < 1) { + grayscale = min / (1 - chroma); + } else { + grayscale = 0; + } + + if (chroma <= 0) { + hue = 0; + } else + if (max === r) { + hue = ((g - b) / chroma) % 6; + } else + if (max === g) { + hue = 2 + (b - r) / chroma; + } else { + hue = 4 + (r - g) / chroma; + } + + hue /= 6; + hue %= 1; + + return [hue * 360, chroma * 100, grayscale * 100]; +}; + +convert.hsl.hcg = function (hsl) { + const s = hsl[1] / 100; + const l = hsl[2] / 100; + + const c = l < 0.5 ? (2.0 * s * l) : (2.0 * s * (1.0 - l)); + + let f = 0; + if (c < 1.0) { + f = (l - 0.5 * c) / (1.0 - c); + } + + return [hsl[0], c * 100, f * 100]; +}; + +convert.hsv.hcg = function (hsv) { + const s = hsv[1] / 100; + const v = hsv[2] / 100; + + const c = s * v; + let f = 0; + + if (c < 1.0) { + f = (v - c) / (1 - c); + } + + return [hsv[0], c * 100, f * 100]; +}; + +convert.hcg.rgb = function (hcg) { + const h = hcg[0] / 360; + const c = hcg[1] / 100; + const g = hcg[2] / 100; + + if (c === 0.0) { + return [g * 255, g * 255, g * 255]; + } + + const pure = [0, 0, 0]; + const hi = (h % 1) * 6; + const v = hi % 1; + const w = 1 - v; + let mg = 0; + + /* eslint-disable max-statements-per-line */ + switch (Math.floor(hi)) { + case 0: + pure[0] = 1; pure[1] = v; pure[2] = 0; break; + case 1: + pure[0] = w; pure[1] = 1; pure[2] = 0; break; + case 2: + pure[0] = 0; pure[1] = 1; pure[2] = v; break; + case 3: + pure[0] = 0; pure[1] = w; pure[2] = 1; break; + case 4: + pure[0] = v; pure[1] = 0; pure[2] = 1; break; + default: + pure[0] = 1; pure[1] = 0; pure[2] = w; + } + /* eslint-enable max-statements-per-line */ + + mg = (1.0 - c) * g; + + return [ + (c * pure[0] + mg) * 255, + (c * pure[1] + mg) * 255, + (c * pure[2] + mg) * 255 + ]; +}; + +convert.hcg.hsv = function (hcg) { + const c = hcg[1] / 100; + const g = hcg[2] / 100; + + const v = c + g * (1.0 - c); + let f = 0; + + if (v > 0.0) { + f = c / v; + } + + return [hcg[0], f * 100, v * 100]; +}; + +convert.hcg.hsl = function (hcg) { + const c = hcg[1] / 100; + const g = hcg[2] / 100; + + const l = g * (1.0 - c) + 0.5 * c; + let s = 0; + + if (l > 0.0 && l < 0.5) { + s = c / (2 * l); + } else + if (l >= 0.5 && l < 1.0) { + s = c / (2 * (1 - l)); + } + + return [hcg[0], s * 100, l * 100]; +}; + +convert.hcg.hwb = function (hcg) { + const c = hcg[1] / 100; + const g = hcg[2] / 100; + const v = c + g * (1.0 - c); + return [hcg[0], (v - c) * 100, (1 - v) * 100]; +}; + +convert.hwb.hcg = function (hwb) { + const w = hwb[1] / 100; + const b = hwb[2] / 100; + const v = 1 - b; + const c = v - w; + let g = 0; + + if (c < 1) { + g = (v - c) / (1 - c); + } + + return [hwb[0], c * 100, g * 100]; +}; + +convert.apple.rgb = function (apple) { + return [(apple[0] / 65535) * 255, (apple[1] / 65535) * 255, (apple[2] / 65535) * 255]; +}; + +convert.rgb.apple = function (rgb) { + return [(rgb[0] / 255) * 65535, (rgb[1] / 255) * 65535, (rgb[2] / 255) * 65535]; +}; + +convert.gray.rgb = function (args) { + return [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255]; +}; + +convert.gray.hsl = function (args) { + return [0, 0, args[0]]; +}; + +convert.gray.hsv = convert.gray.hsl; + +convert.gray.hwb = function (gray) { + return [0, 100, gray[0]]; +}; + +convert.gray.cmyk = function (gray) { + return [0, 0, 0, gray[0]]; +}; + +convert.gray.lab = function (gray) { + return [gray[0], 0, 0]; +}; + +convert.gray.hex = function (gray) { + const val = Math.round(gray[0] / 100 * 255) & 0xFF; + const integer = (val << 16) + (val << 8) + val; + + const string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; +}; + +convert.rgb.gray = function (rgb) { + const val = (rgb[0] + rgb[1] + rgb[2]) / 3; + return [val / 255 * 100]; +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/index.js new file mode 100644 index 00000000..b648e573 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/index.js @@ -0,0 +1,81 @@ +const conversions = require('./conversions'); +const route = require('./route'); + +const convert = {}; + +const models = Object.keys(conversions); + +function wrapRaw(fn) { + const wrappedFn = function (...args) { + const arg0 = args[0]; + if (arg0 === undefined || arg0 === null) { + return arg0; + } + + if (arg0.length > 1) { + args = arg0; + } + + return fn(args); + }; + + // Preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; +} + +function wrapRounded(fn) { + const wrappedFn = function (...args) { + const arg0 = args[0]; + + if (arg0 === undefined || arg0 === null) { + return arg0; + } + + if (arg0.length > 1) { + args = arg0; + } + + const result = fn(args); + + // We're assuming the result is an array here. + // see notice in conversions.js; don't use box types + // in conversion functions. + if (typeof result === 'object') { + for (let len = result.length, i = 0; i < len; i++) { + result[i] = Math.round(result[i]); + } + } + + return result; + }; + + // Preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; +} + +models.forEach(fromModel => { + convert[fromModel] = {}; + + Object.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels}); + Object.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels}); + + const routes = route(fromModel); + const routeModels = Object.keys(routes); + + routeModels.forEach(toModel => { + const fn = routes[toModel]; + + convert[fromModel][toModel] = wrapRounded(fn); + convert[fromModel][toModel].raw = wrapRaw(fn); + }); +}); + +module.exports = convert; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/package.json new file mode 100644 index 00000000..6e48000c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/package.json @@ -0,0 +1,48 @@ +{ + "name": "color-convert", + "description": "Plain color conversion functions", + "version": "2.0.1", + "author": "Heather Arthur ", + "license": "MIT", + "repository": "Qix-/color-convert", + "scripts": { + "pretest": "xo", + "test": "node test/basic.js" + }, + "engines": { + "node": ">=7.0.0" + }, + "keywords": [ + "color", + "colour", + "convert", + "converter", + "conversion", + "rgb", + "hsl", + "hsv", + "hwb", + "cmyk", + "ansi", + "ansi16" + ], + "files": [ + "index.js", + "conversions.js", + "route.js" + ], + "xo": { + "rules": { + "default-case": 0, + "no-inline-comments": 0, + "operator-linebreak": 0 + } + }, + "devDependencies": { + "chalk": "^2.4.2", + "xo": "^0.24.0" + }, + "dependencies": { + "color-name": "~1.1.4" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/route.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/route.js new file mode 100644 index 00000000..1a08521b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-convert/route.js @@ -0,0 +1,97 @@ +const conversions = require('./conversions'); + +/* + This function routes a model to all other models. + + all functions that are routed have a property `.conversion` attached + to the returned synthetic function. This property is an array + of strings, each with the steps in between the 'from' and 'to' + color models (inclusive). + + conversions that are not possible simply are not included. +*/ + +function buildGraph() { + const graph = {}; + // https://jsperf.com/object-keys-vs-for-in-with-closure/3 + const models = Object.keys(conversions); + + for (let len = models.length, i = 0; i < len; i++) { + graph[models[i]] = { + // http://jsperf.com/1-vs-infinity + // micro-opt, but this is simple. + distance: -1, + parent: null + }; + } + + return graph; +} + +// https://en.wikipedia.org/wiki/Breadth-first_search +function deriveBFS(fromModel) { + const graph = buildGraph(); + const queue = [fromModel]; // Unshift -> queue -> pop + + graph[fromModel].distance = 0; + + while (queue.length) { + const current = queue.pop(); + const adjacents = Object.keys(conversions[current]); + + for (let len = adjacents.length, i = 0; i < len; i++) { + const adjacent = adjacents[i]; + const node = graph[adjacent]; + + if (node.distance === -1) { + node.distance = graph[current].distance + 1; + node.parent = current; + queue.unshift(adjacent); + } + } + } + + return graph; +} + +function link(from, to) { + return function (args) { + return to(from(args)); + }; +} + +function wrapConversion(toModel, graph) { + const path = [graph[toModel].parent, toModel]; + let fn = conversions[graph[toModel].parent][toModel]; + + let cur = graph[toModel].parent; + while (graph[cur].parent) { + path.unshift(graph[cur].parent); + fn = link(conversions[graph[cur].parent][cur], fn); + cur = graph[cur].parent; + } + + fn.conversion = path; + return fn; +} + +module.exports = function (fromModel) { + const graph = deriveBFS(fromModel); + const conversion = {}; + + const models = Object.keys(graph); + for (let len = models.length, i = 0; i < len; i++) { + const toModel = models[i]; + const node = graph[toModel]; + + if (node.parent === null) { + // No possible conversion, or this node is the source model. + continue; + } + + conversion[toModel] = wrapConversion(toModel, graph); + } + + return conversion; +}; + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/LICENSE new file mode 100644 index 00000000..c6b10012 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/LICENSE @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/README.md new file mode 100644 index 00000000..932b9791 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/README.md @@ -0,0 +1,11 @@ +A JSON with color names and its values. Based on http://dev.w3.org/csswg/css-color/#named-colors. + +[![NPM](https://nodei.co/npm/color-name.png?mini=true)](https://nodei.co/npm/color-name/) + + +```js +var colors = require('color-name'); +colors.red //[255,0,0] +``` + + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/index.js new file mode 100644 index 00000000..b7c198a6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/index.js @@ -0,0 +1,152 @@ +'use strict' + +module.exports = { + "aliceblue": [240, 248, 255], + "antiquewhite": [250, 235, 215], + "aqua": [0, 255, 255], + "aquamarine": [127, 255, 212], + "azure": [240, 255, 255], + "beige": [245, 245, 220], + "bisque": [255, 228, 196], + "black": [0, 0, 0], + "blanchedalmond": [255, 235, 205], + "blue": [0, 0, 255], + "blueviolet": [138, 43, 226], + "brown": [165, 42, 42], + "burlywood": [222, 184, 135], + "cadetblue": [95, 158, 160], + "chartreuse": [127, 255, 0], + "chocolate": [210, 105, 30], + "coral": [255, 127, 80], + "cornflowerblue": [100, 149, 237], + "cornsilk": [255, 248, 220], + "crimson": [220, 20, 60], + "cyan": [0, 255, 255], + "darkblue": [0, 0, 139], + "darkcyan": [0, 139, 139], + "darkgoldenrod": [184, 134, 11], + "darkgray": [169, 169, 169], + "darkgreen": [0, 100, 0], + "darkgrey": [169, 169, 169], + "darkkhaki": [189, 183, 107], + "darkmagenta": [139, 0, 139], + "darkolivegreen": [85, 107, 47], + "darkorange": [255, 140, 0], + "darkorchid": [153, 50, 204], + "darkred": [139, 0, 0], + "darksalmon": [233, 150, 122], + "darkseagreen": [143, 188, 143], + "darkslateblue": [72, 61, 139], + "darkslategray": [47, 79, 79], + "darkslategrey": [47, 79, 79], + "darkturquoise": [0, 206, 209], + "darkviolet": [148, 0, 211], + "deeppink": [255, 20, 147], + "deepskyblue": [0, 191, 255], + "dimgray": [105, 105, 105], + "dimgrey": [105, 105, 105], + "dodgerblue": [30, 144, 255], + "firebrick": [178, 34, 34], + "floralwhite": [255, 250, 240], + "forestgreen": [34, 139, 34], + "fuchsia": [255, 0, 255], + "gainsboro": [220, 220, 220], + "ghostwhite": [248, 248, 255], + "gold": [255, 215, 0], + "goldenrod": [218, 165, 32], + "gray": [128, 128, 128], + "green": [0, 128, 0], + "greenyellow": [173, 255, 47], + "grey": [128, 128, 128], + "honeydew": [240, 255, 240], + "hotpink": [255, 105, 180], + "indianred": [205, 92, 92], + "indigo": [75, 0, 130], + "ivory": [255, 255, 240], + "khaki": [240, 230, 140], + "lavender": [230, 230, 250], + "lavenderblush": [255, 240, 245], + "lawngreen": [124, 252, 0], + "lemonchiffon": [255, 250, 205], + "lightblue": [173, 216, 230], + "lightcoral": [240, 128, 128], + "lightcyan": [224, 255, 255], + "lightgoldenrodyellow": [250, 250, 210], + "lightgray": [211, 211, 211], + "lightgreen": [144, 238, 144], + "lightgrey": [211, 211, 211], + "lightpink": [255, 182, 193], + "lightsalmon": [255, 160, 122], + "lightseagreen": [32, 178, 170], + "lightskyblue": [135, 206, 250], + "lightslategray": [119, 136, 153], + "lightslategrey": [119, 136, 153], + "lightsteelblue": [176, 196, 222], + "lightyellow": [255, 255, 224], + "lime": [0, 255, 0], + "limegreen": [50, 205, 50], + "linen": [250, 240, 230], + "magenta": [255, 0, 255], + "maroon": [128, 0, 0], + "mediumaquamarine": [102, 205, 170], + "mediumblue": [0, 0, 205], + "mediumorchid": [186, 85, 211], + "mediumpurple": [147, 112, 219], + "mediumseagreen": [60, 179, 113], + "mediumslateblue": [123, 104, 238], + "mediumspringgreen": [0, 250, 154], + "mediumturquoise": [72, 209, 204], + "mediumvioletred": [199, 21, 133], + "midnightblue": [25, 25, 112], + "mintcream": [245, 255, 250], + "mistyrose": [255, 228, 225], + "moccasin": [255, 228, 181], + "navajowhite": [255, 222, 173], + "navy": [0, 0, 128], + "oldlace": [253, 245, 230], + "olive": [128, 128, 0], + "olivedrab": [107, 142, 35], + "orange": [255, 165, 0], + "orangered": [255, 69, 0], + "orchid": [218, 112, 214], + "palegoldenrod": [238, 232, 170], + "palegreen": [152, 251, 152], + "paleturquoise": [175, 238, 238], + "palevioletred": [219, 112, 147], + "papayawhip": [255, 239, 213], + "peachpuff": [255, 218, 185], + "peru": [205, 133, 63], + "pink": [255, 192, 203], + "plum": [221, 160, 221], + "powderblue": [176, 224, 230], + "purple": [128, 0, 128], + "rebeccapurple": [102, 51, 153], + "red": [255, 0, 0], + "rosybrown": [188, 143, 143], + "royalblue": [65, 105, 225], + "saddlebrown": [139, 69, 19], + "salmon": [250, 128, 114], + "sandybrown": [244, 164, 96], + "seagreen": [46, 139, 87], + "seashell": [255, 245, 238], + "sienna": [160, 82, 45], + "silver": [192, 192, 192], + "skyblue": [135, 206, 235], + "slateblue": [106, 90, 205], + "slategray": [112, 128, 144], + "slategrey": [112, 128, 144], + "snow": [255, 250, 250], + "springgreen": [0, 255, 127], + "steelblue": [70, 130, 180], + "tan": [210, 180, 140], + "teal": [0, 128, 128], + "thistle": [216, 191, 216], + "tomato": [255, 99, 71], + "turquoise": [64, 224, 208], + "violet": [238, 130, 238], + "wheat": [245, 222, 179], + "white": [255, 255, 255], + "whitesmoke": [245, 245, 245], + "yellow": [255, 255, 0], + "yellowgreen": [154, 205, 50] +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/package.json new file mode 100644 index 00000000..782dd828 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/color-name/package.json @@ -0,0 +1,28 @@ +{ + "name": "color-name", + "version": "1.1.4", + "description": "A list of color names and its values", + "main": "index.js", + "files": [ + "index.js" + ], + "scripts": { + "test": "node test.js" + }, + "repository": { + "type": "git", + "url": "git@github.com:colorjs/color-name.git" + }, + "keywords": [ + "color-name", + "color", + "color-keyword", + "keyword" + ], + "author": "DY ", + "license": "MIT", + "bugs": { + "url": "https://github.com/colorjs/color-name/issues" + }, + "homepage": "https://github.com/colorjs/color-name" +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/.travis.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/.travis.yml new file mode 100644 index 00000000..f1d0f13c --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - 0.4 + - 0.6 diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/LICENSE new file mode 100644 index 00000000..ee27ba4b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/LICENSE @@ -0,0 +1,18 @@ +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/README.markdown b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/README.markdown new file mode 100644 index 00000000..408f70a1 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/README.markdown @@ -0,0 +1,62 @@ +concat-map +========== + +Concatenative mapdashery. + +[![browser support](http://ci.testling.com/substack/node-concat-map.png)](http://ci.testling.com/substack/node-concat-map) + +[![build status](https://secure.travis-ci.org/substack/node-concat-map.png)](http://travis-ci.org/substack/node-concat-map) + +example +======= + +``` js +var concatMap = require('concat-map'); +var xs = [ 1, 2, 3, 4, 5, 6 ]; +var ys = concatMap(xs, function (x) { + return x % 2 ? [ x - 0.1, x, x + 0.1 ] : []; +}); +console.dir(ys); +``` + +*** + +``` +[ 0.9, 1, 1.1, 2.9, 3, 3.1, 4.9, 5, 5.1 ] +``` + +methods +======= + +``` js +var concatMap = require('concat-map') +``` + +concatMap(xs, fn) +----------------- + +Return an array of concatenated elements by calling `fn(x, i)` for each element +`x` and each index `i` in the array `xs`. + +When `fn(x, i)` returns an array, its result will be concatenated with the +result array. If `fn(x, i)` returns anything else, that value will be pushed +onto the end of the result array. + +install +======= + +With [npm](http://npmjs.org) do: + +``` +npm install concat-map +``` + +license +======= + +MIT + +notes +===== + +This module was written while sitting high above the ground in a tree. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/example/map.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/example/map.js new file mode 100644 index 00000000..33656217 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/example/map.js @@ -0,0 +1,6 @@ +var concatMap = require('../'); +var xs = [ 1, 2, 3, 4, 5, 6 ]; +var ys = concatMap(xs, function (x) { + return x % 2 ? [ x - 0.1, x, x + 0.1 ] : []; +}); +console.dir(ys); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/index.js new file mode 100644 index 00000000..b29a7812 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/index.js @@ -0,0 +1,13 @@ +module.exports = function (xs, fn) { + var res = []; + for (var i = 0; i < xs.length; i++) { + var x = fn(xs[i], i); + if (isArray(x)) res.push.apply(res, x); + else res.push(x); + } + return res; +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/package.json new file mode 100644 index 00000000..d3640e6b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/package.json @@ -0,0 +1,43 @@ +{ + "name" : "concat-map", + "description" : "concatenative mapdashery", + "version" : "0.0.1", + "repository" : { + "type" : "git", + "url" : "git://github.com/substack/node-concat-map.git" + }, + "main" : "index.js", + "keywords" : [ + "concat", + "concatMap", + "map", + "functional", + "higher-order" + ], + "directories" : { + "example" : "example", + "test" : "test" + }, + "scripts" : { + "test" : "tape test/*.js" + }, + "devDependencies" : { + "tape" : "~2.4.0" + }, + "license" : "MIT", + "author" : { + "name" : "James Halliday", + "email" : "mail@substack.net", + "url" : "http://substack.net" + }, + "testling" : { + "files" : "test/*.js", + "browsers" : { + "ie" : [ 6, 7, 8, 9 ], + "ff" : [ 3.5, 10, 15.0 ], + "chrome" : [ 10, 22 ], + "safari" : [ 5.1 ], + "opera" : [ 12 ] + } + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/test/map.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/test/map.js new file mode 100644 index 00000000..fdbd7022 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/concat-map/test/map.js @@ -0,0 +1,39 @@ +var concatMap = require('../'); +var test = require('tape'); + +test('empty or not', function (t) { + var xs = [ 1, 2, 3, 4, 5, 6 ]; + var ixes = []; + var ys = concatMap(xs, function (x, ix) { + ixes.push(ix); + return x % 2 ? [ x - 0.1, x, x + 0.1 ] : []; + }); + t.same(ys, [ 0.9, 1, 1.1, 2.9, 3, 3.1, 4.9, 5, 5.1 ]); + t.same(ixes, [ 0, 1, 2, 3, 4, 5 ]); + t.end(); +}); + +test('always something', function (t) { + var xs = [ 'a', 'b', 'c', 'd' ]; + var ys = concatMap(xs, function (x) { + return x === 'b' ? [ 'B', 'B', 'B' ] : [ x ]; + }); + t.same(ys, [ 'a', 'B', 'B', 'B', 'c', 'd' ]); + t.end(); +}); + +test('scalars', function (t) { + var xs = [ 'a', 'b', 'c', 'd' ]; + var ys = concatMap(xs, function (x) { + return x === 'b' ? [ 'B', 'B', 'B' ] : x; + }); + t.same(ys, [ 'a', 'B', 'B', 'B', 'c', 'd' ]); + t.end(); +}); + +test('undefs', function (t) { + var xs = [ 'a', 'b', 'c', 'd' ]; + var ys = concatMap(xs, function () {}); + t.same(ys, [ undefined, undefined, undefined, undefined ]); + t.end(); +}); diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/HISTORY.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/HISTORY.md new file mode 100644 index 00000000..1a3b308b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/HISTORY.md @@ -0,0 +1,72 @@ +1.0.1 / 2025-11-18 +================= + + * Updated `engines` field to Node@18 or higher (fixed reference, see 1.0.0) + * Remove dependency `safe-buffer` + +1.0.0 / 2024-08-31 +================== + + * drop node <18 + * allow utf8 as alias for utf-8 + +0.5.4 / 2021-12-10 +================== + + * deps: safe-buffer@5.2.1 + +0.5.3 / 2018-12-17 +================== + + * Use `safe-buffer` for improved Buffer API + +0.5.2 / 2016-12-08 +================== + + * Fix `parse` to accept any linear whitespace character + +0.5.1 / 2016-01-17 +================== + + * perf: enable strict mode + +0.5.0 / 2014-10-11 +================== + + * Add `parse` function + +0.4.0 / 2014-09-21 +================== + + * Expand non-Unicode `filename` to the full ISO-8859-1 charset + +0.3.0 / 2014-09-20 +================== + + * Add `fallback` option + * Add `type` option + +0.2.0 / 2014-09-19 +================== + + * Reduce ambiguity of file names with hex escape in buggy browsers + +0.1.2 / 2014-09-19 +================== + + * Fix periodic invalid Unicode filename header + +0.1.1 / 2014-09-19 +================== + + * Fix invalid characters appearing in `filename*` parameter + +0.1.0 / 2014-09-18 +================== + + * Make the `filename` argument optional + +0.0.0 / 2014-09-18 +================== + + * Initial release diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/LICENSE new file mode 100644 index 00000000..84441fbb --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/README.md new file mode 100644 index 00000000..fbedc2f8 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/README.md @@ -0,0 +1,142 @@ +# content-disposition + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Node.js Version][node-version-image]][node-version-url] +[![Build Status][github-actions-ci-image]][github-actions-ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Create and parse HTTP `Content-Disposition` header + +## Installation + +```sh +$ npm install content-disposition +``` + +## API + +```js +const contentDisposition = require('content-disposition') +``` + +### contentDisposition(filename, options) + +Create an attachment `Content-Disposition` header value using the given file name, +if supplied. The `filename` is optional and if no file name is desired, but you +want to specify `options`, set `filename` to `undefined`. + +```js +res.setHeader('Content-Disposition', contentDisposition('∫ maths.pdf')) +``` + +**note** HTTP headers are of the ISO-8859-1 character set. If you are writing this +header through a means different from `setHeader` in Node.js, you'll want to specify +the `'binary'` encoding in Node.js. + +#### Options + +`contentDisposition` accepts these properties in the options object. + +##### fallback + +If the `filename` option is outside ISO-8859-1, then the file name is actually +stored in a supplemental field for clients that support Unicode file names and +a ISO-8859-1 version of the file name is automatically generated. + +This specifies the ISO-8859-1 file name to override the automatic generation or +disables the generation all together, defaults to `true`. + + - A string will specify the ISO-8859-1 file name to use in place of automatic + generation. + - `false` will disable including a ISO-8859-1 file name and only include the + Unicode version (unless the file name is already ISO-8859-1). + - `true` will enable automatic generation if the file name is outside ISO-8859-1. + +If the `filename` option is ISO-8859-1 and this option is specified and has a +different value, then the `filename` option is encoded in the extended field +and this set as the fallback field, even though they are both ISO-8859-1. + +##### type + +Specifies the disposition type, defaults to `"attachment"`. This can also be +`"inline"`, or any other value (all values except inline are treated like +`attachment`, but can convey additional information if both parties agree to +it). The type is normalized to lower-case. + +### contentDisposition.parse(string) + +```js +const disposition = contentDisposition.parse('attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt') +``` + +Parse a `Content-Disposition` header string. This automatically handles extended +("Unicode") parameters by decoding them and providing them under the standard +parameter name. This will return an object with the following properties (examples +are shown for the string `'attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt'`): + + - `type`: The disposition type (always lower case). Example: `'attachment'` + + - `parameters`: An object of the parameters in the disposition (name of parameter + always lower case and extended versions replace non-extended versions). Example: + `{filename: "€ rates.txt"}` + +## Examples + +### Send a file for download + +```js +const contentDisposition = require('content-disposition') +const destroy = require('destroy') +const fs = require('fs') +const http = require('http') +const onFinished = require('on-finished') + +const filePath = '/path/to/public/plans.pdf' + +http.createServer(function onRequest (req, res) { + // set headers + res.setHeader('Content-Type', 'application/pdf') + res.setHeader('Content-Disposition', contentDisposition(filePath)) + + // send file + const stream = fs.createReadStream(filePath) + stream.pipe(res) + onFinished(res, function () { + destroy(stream) + }) +}) +``` + +## Testing + +```sh +$ npm test +``` + +## References + +- [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1][rfc-2616] +- [RFC 5987: Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters][rfc-5987] +- [RFC 6266: Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)][rfc-6266] +- [Test Cases for HTTP Content-Disposition header field (RFC 6266) and the Encodings defined in RFCs 2047, 2231 and 5987][tc-2231] + +[rfc-2616]: https://tools.ietf.org/html/rfc2616 +[rfc-5987]: https://tools.ietf.org/html/rfc5987 +[rfc-6266]: https://tools.ietf.org/html/rfc6266 +[tc-2231]: http://greenbytes.de/tech/tc2231/ + +## License + +[MIT](LICENSE) + +[npm-image]: https://img.shields.io/npm/v/content-disposition +[npm-url]: https://npmjs.org/package/content-disposition +[node-version-image]: https://img.shields.io/node/v/content-disposition +[node-version-url]: https://nodejs.org/en/download +[coveralls-image]: https://img.shields.io/coverallsCoverage/github/jshttp/content-disposition +[coveralls-url]: https://coveralls.io/r/jshttp/content-disposition?branch=master +[downloads-image]: https://img.shields.io/npm/dm/content-disposition +[downloads-url]: https://npmjs.org/package/content-disposition +[github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-disposition/ci.yml +[github-actions-ci-url]: https://github.com/jshttp/content-disposition/actions/workflows/ci.yml diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/index.js new file mode 100644 index 00000000..efcd9ca7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/index.js @@ -0,0 +1,458 @@ +/*! + * content-disposition + * Copyright(c) 2014-2017 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = contentDisposition +module.exports.parse = parse + +/** + * Module dependencies. + * @private + */ + +var basename = require('path').basename + +/** + * RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%") + * @private + */ + +var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex + +/** + * RegExp to match percent encoding escape. + * @private + */ + +var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/ +var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g + +/** + * RegExp to match non-latin1 characters. + * @private + */ + +var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g + +/** + * RegExp to match quoted-pair in RFC 2616 + * + * quoted-pair = "\" CHAR + * CHAR = + * @private + */ + +var QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex + +/** + * RegExp to match chars that must be quoted-pair in RFC 2616 + * @private + */ + +var QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp for various RFC 2616 grammar + * + * parameter = token "=" ( token | quoted-string ) + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + * qdtext = > + * quoted-pair = "\" CHAR + * CHAR = + * TEXT = + * LWS = [CRLF] 1*( SP | HT ) + * CRLF = CR LF + * CR = + * LF = + * SP = + * HT = + * CTL = + * OCTET = + * @private + */ + +var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex +var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/ +var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/ + +/** + * RegExp for various RFC 5987 grammar + * + * ext-value = charset "'" [ language ] "'" value-chars + * charset = "UTF-8" / "ISO-8859-1" / mime-charset + * mime-charset = 1*mime-charsetc + * mime-charsetc = ALPHA / DIGIT + * / "!" / "#" / "$" / "%" / "&" + * / "+" / "-" / "^" / "_" / "`" + * / "{" / "}" / "~" + * language = ( 2*3ALPHA [ extlang ] ) + * / 4ALPHA + * / 5*8ALPHA + * extlang = *3( "-" 3ALPHA ) + * value-chars = *( pct-encoded / attr-char ) + * pct-encoded = "%" HEXDIG HEXDIG + * attr-char = ALPHA / DIGIT + * / "!" / "#" / "$" / "&" / "+" / "-" / "." + * / "^" / "_" / "`" / "|" / "~" + * @private + */ + +var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/ + +/** + * RegExp for various RFC 6266 grammar + * + * disposition-type = "inline" | "attachment" | disp-ext-type + * disp-ext-type = token + * disposition-parm = filename-parm | disp-ext-parm + * filename-parm = "filename" "=" value + * | "filename*" "=" ext-value + * disp-ext-parm = token "=" value + * | ext-token "=" ext-value + * ext-token = + * @private + */ + +var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex + +/** + * Create an attachment Content-Disposition header. + * + * @param {string} [filename] + * @param {object} [options] + * @param {string} [options.type=attachment] + * @param {string|boolean} [options.fallback=true] + * @return {string} + * @public + */ + +function contentDisposition (filename, options) { + var opts = options || {} + + // get type + var type = opts.type || 'attachment' + + // get parameters + var params = createparams(filename, opts.fallback) + + // format into string + return format(new ContentDisposition(type, params)) +} + +/** + * Create parameters object from filename and fallback. + * + * @param {string} [filename] + * @param {string|boolean} [fallback=true] + * @return {object} + * @private + */ + +function createparams (filename, fallback) { + if (filename === undefined) { + return + } + + var params = {} + + if (typeof filename !== 'string') { + throw new TypeError('filename must be a string') + } + + // fallback defaults to true + if (fallback === undefined) { + fallback = true + } + + if (typeof fallback !== 'string' && typeof fallback !== 'boolean') { + throw new TypeError('fallback must be a string or boolean') + } + + if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) { + throw new TypeError('fallback must be ISO-8859-1 string') + } + + // restrict to file base name + var name = basename(filename) + + // determine if name is suitable for quoted string + var isQuotedString = TEXT_REGEXP.test(name) + + // generate fallback name + var fallbackName = typeof fallback !== 'string' + ? fallback && getlatin1(name) + : basename(fallback) + var hasFallback = typeof fallbackName === 'string' && fallbackName !== name + + // set extended filename parameter + if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) { + params['filename*'] = name + } + + // set filename parameter + if (isQuotedString || hasFallback) { + params.filename = hasFallback + ? fallbackName + : name + } + + return params +} + +/** + * Format object to Content-Disposition header. + * + * @param {object} obj + * @param {string} obj.type + * @param {object} [obj.parameters] + * @return {string} + * @private + */ + +function format (obj) { + var parameters = obj.parameters + var type = obj.type + + if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + // start with normalized type + var string = String(type).toLowerCase() + + // append parameters + if (parameters && typeof parameters === 'object') { + var param + var params = Object.keys(parameters).sort() + + for (var i = 0; i < params.length; i++) { + param = params[i] + + var val = param.slice(-1) === '*' + ? ustring(parameters[param]) + : qstring(parameters[param]) + + string += '; ' + param + '=' + val + } + } + + return string +} + +/** + * Decode a RFC 5987 field value (gracefully). + * + * @param {string} str + * @return {string} + * @private + */ + +function decodefield (str) { + var match = EXT_VALUE_REGEXP.exec(str) + + if (!match) { + throw new TypeError('invalid extended field value') + } + + var charset = match[1].toLowerCase() + var encoded = match[2] + var value + + // to binary string + var binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode) + + switch (charset) { + case 'iso-8859-1': + value = getlatin1(binary) + break + case 'utf-8': + case 'utf8': + value = Buffer.from(binary, 'binary').toString('utf8') + break + default: + throw new TypeError('unsupported charset in extended field') + } + + return value +} + +/** + * Get ISO-8859-1 version of string. + * + * @param {string} val + * @return {string} + * @private + */ + +function getlatin1 (val) { + // simple Unicode -> ISO-8859-1 transformation + return String(val).replace(NON_LATIN1_REGEXP, '?') +} + +/** + * Parse Content-Disposition header string. + * + * @param {string} string + * @return {object} + * @public + */ + +function parse (string) { + if (!string || typeof string !== 'string') { + throw new TypeError('argument string is required') + } + + var match = DISPOSITION_TYPE_REGEXP.exec(string) + + if (!match) { + throw new TypeError('invalid type format') + } + + // normalize type + var index = match[0].length + var type = match[1].toLowerCase() + + var key + var names = [] + var params = {} + var value + + // calculate index to start at + index = PARAM_REGEXP.lastIndex = match[0].slice(-1) === ';' + ? index - 1 + : index + + // match parameters + while ((match = PARAM_REGEXP.exec(string))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (names.indexOf(key) !== -1) { + throw new TypeError('invalid duplicate parameter') + } + + names.push(key) + + if (key.indexOf('*') + 1 === key.length) { + // decode extended value + key = key.slice(0, -1) + value = decodefield(value) + + // overwrite existing value + params[key] = value + continue + } + + if (typeof params[key] === 'string') { + continue + } + + if (value[0] === '"') { + // remove quotes and escapes + value = value + .slice(1, -1) + .replace(QESC_REGEXP, '$1') + } + + params[key] = value + } + + if (index !== -1 && index !== string.length) { + throw new TypeError('invalid parameter format') + } + + return new ContentDisposition(type, params) +} + +/** + * Percent decode a single character. + * + * @param {string} str + * @param {string} hex + * @return {string} + * @private + */ + +function pdecode (str, hex) { + return String.fromCharCode(parseInt(hex, 16)) +} + +/** + * Percent encode a single character. + * + * @param {string} char + * @return {string} + * @private + */ + +function pencode (char) { + return '%' + String(char) + .charCodeAt(0) + .toString(16) + .toUpperCase() +} + +/** + * Quote a string for HTTP. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + var str = String(val) + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Encode a Unicode string for HTTP (RFC 5987). + * + * @param {string} val + * @return {string} + * @private + */ + +function ustring (val) { + var str = String(val) + + // percent encode as UTF-8 + var encoded = encodeURIComponent(str) + .replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode) + + return 'UTF-8\'\'' + encoded +} + +/** + * Class for parsed Content-Disposition header for v8 optimization + * + * @public + * @param {string} type + * @param {object} parameters + * @constructor + */ + +function ContentDisposition (type, parameters) { + this.type = type + this.parameters = parameters +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/package.json new file mode 100644 index 00000000..a44034cf --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-disposition/package.json @@ -0,0 +1,43 @@ +{ + "name": "content-disposition", + "description": "Create and parse Content-Disposition header", + "version": "1.0.1", + "author": "Douglas Christopher Wilson ", + "license": "MIT", + "keywords": [ + "content-disposition", + "http", + "rfc6266", + "res" + ], + "repository": "jshttp/content-disposition", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, + "devDependencies": { + "c8": "^10.1.2", + "eslint": "7.32.0", + "eslint-config-standard": "13.0.1", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "5.2.0", + "eslint-plugin-standard": "4.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "lint": "eslint .", + "test": "node --test --test-reporter spec", + "test-ci": "c8 --reporter=lcovonly --reporter=text npm test", + "test-cov": "c8 --reporter=html --reporter=text npm test" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/HISTORY.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/HISTORY.md new file mode 100644 index 00000000..45836713 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/HISTORY.md @@ -0,0 +1,29 @@ +1.0.5 / 2023-01-29 +================== + + * perf: skip value escaping when unnecessary + +1.0.4 / 2017-09-11 +================== + + * perf: skip parameter parsing when no parameters + +1.0.3 / 2017-09-10 +================== + + * perf: remove argument reassignment + +1.0.2 / 2016-05-09 +================== + + * perf: enable strict mode + +1.0.1 / 2015-02-13 +================== + + * Improve missing `Content-Type` header error message + +1.0.0 / 2015-02-01 +================== + + * Initial implementation, derived from `media-typer@0.3.0` diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/LICENSE new file mode 100644 index 00000000..34b1a2de --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/README.md new file mode 100644 index 00000000..c1a922a9 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/README.md @@ -0,0 +1,94 @@ +# content-type + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-image]][node-url] +[![Build Status][ci-image]][ci-url] +[![Coverage Status][coveralls-image]][coveralls-url] + +Create and parse HTTP Content-Type header according to RFC 7231 + +## Installation + +```sh +$ npm install content-type +``` + +## API + +```js +var contentType = require('content-type') +``` + +### contentType.parse(string) + +```js +var obj = contentType.parse('image/svg+xml; charset=utf-8') +``` + +Parse a `Content-Type` header. This will return an object with the following +properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`): + + - `type`: The media type (the type and subtype, always lower case). + Example: `'image/svg+xml'` + + - `parameters`: An object of the parameters in the media type (name of parameter + always lower case). Example: `{charset: 'utf-8'}` + +Throws a `TypeError` if the string is missing or invalid. + +### contentType.parse(req) + +```js +var obj = contentType.parse(req) +``` + +Parse the `Content-Type` header from the given `req`. Short-cut for +`contentType.parse(req.headers['content-type'])`. + +Throws a `TypeError` if the `Content-Type` header is missing or invalid. + +### contentType.parse(res) + +```js +var obj = contentType.parse(res) +``` + +Parse the `Content-Type` header set on the given `res`. Short-cut for +`contentType.parse(res.getHeader('content-type'))`. + +Throws a `TypeError` if the `Content-Type` header is missing or invalid. + +### contentType.format(obj) + +```js +var str = contentType.format({ + type: 'image/svg+xml', + parameters: { charset: 'utf-8' } +}) +``` + +Format an object into a `Content-Type` header. This will return a string of the +content type for the given object with the following properties (examples are +shown that produce the string `'image/svg+xml; charset=utf-8'`): + + - `type`: The media type (will be lower-cased). Example: `'image/svg+xml'` + + - `parameters`: An object of the parameters in the media type (name of the + parameter will be lower-cased). Example: `{charset: 'utf-8'}` + +Throws a `TypeError` if the object contains an invalid type or parameter names. + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/jshttp/content-type/master?label=ci +[ci-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/content-type/master +[coveralls-url]: https://coveralls.io/r/jshttp/content-type?branch=master +[node-image]: https://badgen.net/npm/node/content-type +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/content-type +[npm-url]: https://npmjs.org/package/content-type +[npm-version-image]: https://badgen.net/npm/v/content-type diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/index.js new file mode 100644 index 00000000..41840e7b --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/index.js @@ -0,0 +1,225 @@ +/*! + * content-type + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1 + * + * parameter = token "=" ( token / quoted-string ) + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + * / DIGIT / ALPHA + * ; any VCHAR, except delimiters + * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + * obs-text = %x80-FF + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + */ +var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex +var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex +var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * RegExp to match quoted-pair in RFC 7230 sec 3.2.6 + * + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + * obs-text = %x80-FF + */ +var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex + +/** + * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6 + */ +var QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp to match type in RFC 7231 sec 3.1.1.1 + * + * media-type = type "/" subtype + * type = token + * subtype = token + */ +var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * Module exports. + * @public + */ + +exports.format = format +exports.parse = parse + +/** + * Format object to media type. + * + * @param {object} obj + * @return {string} + * @public + */ + +function format (obj) { + if (!obj || typeof obj !== 'object') { + throw new TypeError('argument obj is required') + } + + var parameters = obj.parameters + var type = obj.type + + if (!type || !TYPE_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + var string = type + + // append parameters + if (parameters && typeof parameters === 'object') { + var param + var params = Object.keys(parameters).sort() + + for (var i = 0; i < params.length; i++) { + param = params[i] + + if (!TOKEN_REGEXP.test(param)) { + throw new TypeError('invalid parameter name') + } + + string += '; ' + param + '=' + qstring(parameters[param]) + } + } + + return string +} + +/** + * Parse media type to object. + * + * @param {string|object} string + * @return {Object} + * @public + */ + +function parse (string) { + if (!string) { + throw new TypeError('argument string is required') + } + + // support req/res-like objects as argument + var header = typeof string === 'object' + ? getcontenttype(string) + : string + + if (typeof header !== 'string') { + throw new TypeError('argument string is required to be a string') + } + + var index = header.indexOf(';') + var type = index !== -1 + ? header.slice(0, index).trim() + : header.trim() + + if (!TYPE_REGEXP.test(type)) { + throw new TypeError('invalid media type') + } + + var obj = new ContentType(type.toLowerCase()) + + // parse parameters + if (index !== -1) { + var key + var match + var value + + PARAM_REGEXP.lastIndex = index + + while ((match = PARAM_REGEXP.exec(header))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (value.charCodeAt(0) === 0x22 /* " */) { + // remove quotes + value = value.slice(1, -1) + + // remove escapes + if (value.indexOf('\\') !== -1) { + value = value.replace(QESC_REGEXP, '$1') + } + } + + obj.parameters[key] = value + } + + if (index !== header.length) { + throw new TypeError('invalid parameter format') + } + } + + return obj +} + +/** + * Get content-type from req/res objects. + * + * @param {object} + * @return {Object} + * @private + */ + +function getcontenttype (obj) { + var header + + if (typeof obj.getHeader === 'function') { + // res-like + header = obj.getHeader('content-type') + } else if (typeof obj.headers === 'object') { + // req-like + header = obj.headers && obj.headers['content-type'] + } + + if (typeof header !== 'string') { + throw new TypeError('content-type header is missing from object') + } + + return header +} + +/** + * Quote a string if necessary. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + var str = String(val) + + // no need to quote tokens + if (TOKEN_REGEXP.test(str)) { + return str + } + + if (str.length > 0 && !TEXT_REGEXP.test(str)) { + throw new TypeError('invalid parameter value') + } + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Class to represent a content type. + * @private + */ +function ContentType (type) { + this.parameters = Object.create(null) + this.type = type +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/package.json new file mode 100644 index 00000000..9db19f63 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/content-type/package.json @@ -0,0 +1,42 @@ +{ + "name": "content-type", + "description": "Create and parse HTTP Content-Type header", + "version": "1.0.5", + "author": "Douglas Christopher Wilson ", + "license": "MIT", + "keywords": [ + "content-type", + "http", + "req", + "res", + "rfc7231" + ], + "repository": "jshttp/content-type", + "devDependencies": { + "deep-equal": "1.0.1", + "eslint": "8.32.0", + "eslint-config-standard": "15.0.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "6.1.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "10.2.0", + "nyc": "15.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --check-leaks --bail test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/History.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/History.md new file mode 100644 index 00000000..479211a7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/History.md @@ -0,0 +1,70 @@ +1.2.2 / 2024-10-29 +================== + +* various metadata/documentation tweaks (incl. #51) + + +1.2.1 / 2023-02-27 +================== + +* update annotations for allowed secret key types (#44, thanks @jyasskin!) + + +1.2.0 / 2022-02-17 +================== + +* allow buffer and other node-supported types as key (#33) +* be pickier about extra content after signed portion (#40) +* some internal code clarity/cleanup improvements (#26) + + +1.1.0 / 2018-01-18 +================== + +* switch to built-in `crypto.timingSafeEqual` for validation instead of previous double-hash method (thank you @jodevsa!) + + +1.0.7 / 2023-04-12 +================== + +Later release for older node.js versions. See the [v1.0.x branch notes](https://github.com/tj/node-cookie-signature/blob/v1.0.x/History.md#107--2023-04-12). + + +1.0.6 / 2015-02-03 +================== + +* use `npm test` instead of `make test` to run tests +* clearer assertion messages when checking input + + +1.0.5 / 2014-09-05 +================== + +* add license to package.json + +1.0.4 / 2014-06-25 +================== + + * corrected avoidance of timing attacks (thanks @tenbits!) + +1.0.3 / 2014-01-28 +================== + + * [incorrect] fix for timing attacks + +1.0.2 / 2014-01-28 +================== + + * fix missing repository warning + * fix typo in test + +1.0.1 / 2013-04-15 +================== + + * Revert "Changed underlying HMAC algo. to sha512." + * Revert "Fix for timing attacks on MAC verification." + +0.0.1 / 2010-01-03 +================== + + * Initial release diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/LICENSE new file mode 100644 index 00000000..a2671bf7 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2012–2024 LearnBoost and other contributors; + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/Readme.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/Readme.md new file mode 100644 index 00000000..369af15f --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/Readme.md @@ -0,0 +1,23 @@ + +# cookie-signature + + Sign and unsign cookies. + +## Example + +```js +var cookie = require('cookie-signature'); + +var val = cookie.sign('hello', 'tobiiscool'); +val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI'); + +var val = cookie.sign('hello', 'tobiiscool'); +cookie.unsign(val, 'tobiiscool').should.equal('hello'); +cookie.unsign(val, 'luna').should.be.false; +``` + +## License + +MIT. + +See LICENSE file for details. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/index.js new file mode 100644 index 00000000..3fbbddb6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/index.js @@ -0,0 +1,47 @@ +/** + * Module dependencies. + */ + +var crypto = require('crypto'); + +/** + * Sign the given `val` with `secret`. + * + * @param {String} val + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String} + * @api private + */ + +exports.sign = function(val, secret){ + if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string."); + if (null == secret) throw new TypeError("Secret key must be provided."); + return val + '.' + crypto + .createHmac('sha256', secret) + .update(val) + .digest('base64') + .replace(/\=+$/, ''); +}; + +/** + * Unsign and decode the given `input` with `secret`, + * returning `false` if the signature is invalid. + * + * @param {String} input + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String|Boolean} + * @api private + */ + +exports.unsign = function(input, secret){ + if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided."); + if (null == secret) throw new TypeError("Secret key must be provided."); + var tentativeValue = input.slice(0, input.lastIndexOf('.')), + expectedInput = exports.sign(tentativeValue, secret), + expectedBuffer = Buffer.from(expectedInput), + inputBuffer = Buffer.from(input); + return ( + expectedBuffer.length === inputBuffer.length && + crypto.timingSafeEqual(expectedBuffer, inputBuffer) + ) ? tentativeValue : false; +}; diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/package.json new file mode 100644 index 00000000..a1600400 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie-signature/package.json @@ -0,0 +1,24 @@ +{ + "name": "cookie-signature", + "version": "1.2.2", + "main": "index.js", + "description": "Sign and unsign cookies", + "keywords": ["cookie", "sign", "unsign"], + "author": "TJ Holowaychuk ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/visionmedia/node-cookie-signature.git" + }, + "dependencies": {}, + "engines": { + "node": ">=6.6.0" + }, + "devDependencies": { + "mocha": "*", + "should": "*" + }, + "scripts": { + "test": "mocha --require should --reporter spec" + } +} diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/LICENSE new file mode 100644 index 00000000..058b6b4e --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/README.md new file mode 100644 index 00000000..71fdac11 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/README.md @@ -0,0 +1,317 @@ +# cookie + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-image]][node-url] +[![Build Status][ci-image]][ci-url] +[![Coverage Status][coveralls-image]][coveralls-url] + +Basic HTTP cookie parser and serializer for HTTP servers. + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install cookie +``` + +## API + +```js +var cookie = require('cookie'); +``` + +### cookie.parse(str, options) + +Parse an HTTP `Cookie` header string and returning an object of all cookie name-value pairs. +The `str` argument is the string representing a `Cookie` header value and `options` is an +optional object containing additional parsing options. + +```js +var cookies = cookie.parse('foo=bar; equation=E%3Dmc%5E2'); +// { foo: 'bar', equation: 'E=mc^2' } +``` + +#### Options + +`cookie.parse` accepts these properties in the options object. + +##### decode + +Specifies a function that will be used to decode a cookie's value. Since the value of a cookie +has a limited character set (and must be a simple string), this function can be used to decode +a previously-encoded cookie value into a JavaScript string or other object. + +The default function is the global `decodeURIComponent`, which will decode any URL-encoded +sequences into their byte representations. + +**note** if an error is thrown from this function, the original, non-decoded cookie value will +be returned as the cookie's value. + +### cookie.serialize(name, value, options) + +Serialize a cookie name-value pair into a `Set-Cookie` header string. The `name` argument is the +name for the cookie, the `value` argument is the value to set the cookie to, and the `options` +argument is an optional object containing additional serialization options. + +```js +var setCookie = cookie.serialize('foo', 'bar'); +// foo=bar +``` + +#### Options + +`cookie.serialize` accepts these properties in the options object. + +##### domain + +Specifies the value for the [`Domain` `Set-Cookie` attribute][rfc-6265-5.2.3]. By default, no +domain is set, and most clients will consider the cookie to apply to only the current domain. + +##### encode + +Specifies a function that will be used to encode a cookie's value. Since value of a cookie +has a limited character set (and must be a simple string), this function can be used to encode +a value into a string suited for a cookie's value. + +The default function is the global `encodeURIComponent`, which will encode a JavaScript string +into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range. + +##### expires + +Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute][rfc-6265-5.2.1]. +By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and +will delete it on a condition like exiting a web browser application. + +**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### httpOnly + +Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute][rfc-6265-5.2.6]. When truthy, +the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set. + +**note** be careful when setting this to `true`, as compliant clients will not allow client-side +JavaScript to see the cookie in `document.cookie`. + +##### maxAge + +Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute][rfc-6265-5.2.2]. +The given number will be converted to an integer by rounding down. By default, no maximum age is set. + +**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### partitioned + +Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies) +attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the +`Partitioned` attribute is not set. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +More information about can be found in [the proposal](https://github.com/privacycg/CHIPS). + +##### path + +Specifies the value for the [`Path` `Set-Cookie` attribute][rfc-6265-5.2.4]. By default, the path +is considered the ["default path"][rfc-6265-5.1.4]. + +##### priority + +Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1]. + + - `'low'` will set the `Priority` attribute to `Low`. + - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set. + - `'high'` will set the `Priority` attribute to `High`. + +More information about the different priority levels can be found in +[the specification][rfc-west-cookie-priority-00-4.1]. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +##### sameSite + +Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute][rfc-6265bis-09-5.4.7]. + + - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + - `false` will not set the `SameSite` attribute. + - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. + - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. + - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + +More information about the different enforcement levels can be found in +[the specification][rfc-6265bis-09-5.4.7]. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +##### secure + +Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute][rfc-6265-5.2.5]. When truthy, +the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. + +**note** be careful when setting this to `true`, as compliant clients will not send the cookie back to +the server in the future if the browser does not have an HTTPS connection. + +## Example + +The following example uses this module in conjunction with the Node.js core HTTP server +to prompt a user for their name and display it back on future visits. + +```js +var cookie = require('cookie'); +var escapeHtml = require('escape-html'); +var http = require('http'); +var url = require('url'); + +function onRequest(req, res) { + // Parse the query string + var query = url.parse(req.url, true, true).query; + + if (query && query.name) { + // Set a new cookie with the name + res.setHeader('Set-Cookie', cookie.serialize('name', String(query.name), { + httpOnly: true, + maxAge: 60 * 60 * 24 * 7 // 1 week + })); + + // Redirect back after setting cookie + res.statusCode = 302; + res.setHeader('Location', req.headers.referer || '/'); + res.end(); + return; + } + + // Parse the cookies on the request + var cookies = cookie.parse(req.headers.cookie || ''); + + // Get the visitor name set in the cookie + var name = cookies.name; + + res.setHeader('Content-Type', 'text/html; charset=UTF-8'); + + if (name) { + res.write('

Welcome back, ' + escapeHtml(name) + '!

'); + } else { + res.write('

Hello, new visitor!

'); + } + + res.write('
'); + res.write(' '); + res.end('
'); +} + +http.createServer(onRequest).listen(3000); +``` + +## Testing + +```sh +$ npm test +``` + +## Benchmark + +``` +$ npm run bench + +> cookie@0.5.0 bench +> node benchmark/index.js + + node@18.18.2 + acorn@8.10.0 + ada@2.6.0 + ares@1.19.1 + brotli@1.0.9 + cldr@43.1 + icu@73.2 + llhttp@6.0.11 + modules@108 + napi@9 + nghttp2@1.57.0 + nghttp3@0.7.0 + ngtcp2@0.8.1 + openssl@3.0.10+quic + simdutf@3.2.14 + tz@2023c + undici@5.26.3 + unicode@15.0 + uv@1.44.2 + uvwasi@0.0.18 + v8@10.2.154.26-node.26 + zlib@1.2.13.1-motley + +> node benchmark/parse-top.js + + cookie.parse - top sites + + 14 tests completed. + + parse accounts.google.com x 2,588,913 ops/sec ±0.74% (186 runs sampled) + parse apple.com x 2,370,002 ops/sec ±0.69% (186 runs sampled) + parse cloudflare.com x 2,213,102 ops/sec ±0.88% (188 runs sampled) + parse docs.google.com x 2,194,157 ops/sec ±1.03% (184 runs sampled) + parse drive.google.com x 2,265,084 ops/sec ±0.79% (187 runs sampled) + parse en.wikipedia.org x 457,099 ops/sec ±0.81% (186 runs sampled) + parse linkedin.com x 504,407 ops/sec ±0.89% (186 runs sampled) + parse maps.google.com x 1,230,959 ops/sec ±0.98% (186 runs sampled) + parse microsoft.com x 926,294 ops/sec ±0.88% (184 runs sampled) + parse play.google.com x 2,311,338 ops/sec ±0.83% (185 runs sampled) + parse support.google.com x 1,508,850 ops/sec ±0.86% (186 runs sampled) + parse www.google.com x 1,022,582 ops/sec ±1.32% (182 runs sampled) + parse youtu.be x 332,136 ops/sec ±1.02% (185 runs sampled) + parse youtube.com x 323,833 ops/sec ±0.77% (183 runs sampled) + +> node benchmark/parse.js + + cookie.parse - generic + + 6 tests completed. + + simple x 3,214,032 ops/sec ±1.61% (183 runs sampled) + decode x 587,237 ops/sec ±1.16% (187 runs sampled) + unquote x 2,954,618 ops/sec ±1.35% (183 runs sampled) + duplicates x 857,008 ops/sec ±0.89% (187 runs sampled) + 10 cookies x 292,133 ops/sec ±0.89% (187 runs sampled) + 100 cookies x 22,610 ops/sec ±0.68% (187 runs sampled) +``` + +## References + +- [RFC 6265: HTTP State Management Mechanism][rfc-6265] +- [Same-site Cookies][rfc-6265bis-09-5.4.7] + +[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/ +[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1 +[rfc-6265bis-09-5.4.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7 +[rfc-6265]: https://tools.ietf.org/html/rfc6265 +[rfc-6265-5.1.4]: https://tools.ietf.org/html/rfc6265#section-5.1.4 +[rfc-6265-5.2.1]: https://tools.ietf.org/html/rfc6265#section-5.2.1 +[rfc-6265-5.2.2]: https://tools.ietf.org/html/rfc6265#section-5.2.2 +[rfc-6265-5.2.3]: https://tools.ietf.org/html/rfc6265#section-5.2.3 +[rfc-6265-5.2.4]: https://tools.ietf.org/html/rfc6265#section-5.2.4 +[rfc-6265-5.2.5]: https://tools.ietf.org/html/rfc6265#section-5.2.5 +[rfc-6265-5.2.6]: https://tools.ietf.org/html/rfc6265#section-5.2.6 +[rfc-6265-5.3]: https://tools.ietf.org/html/rfc6265#section-5.3 + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/jshttp/cookie/master?label=ci +[ci-url]: https://github.com/jshttp/cookie/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/cookie/master +[coveralls-url]: https://coveralls.io/r/jshttp/cookie?branch=master +[node-image]: https://badgen.net/npm/node/cookie +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/cookie +[npm-url]: https://npmjs.org/package/cookie +[npm-version-image]: https://badgen.net/npm/v/cookie diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/SECURITY.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/SECURITY.md new file mode 100644 index 00000000..fd4a6c53 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policies and Procedures + +## Reporting a Bug + +The `cookie` team and community take all security bugs seriously. Thank +you for improving the security of the project. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by emailing the current owner(s) of `cookie`. This +information can be found in the npm registry using the command +`npm owner ls cookie`. +If unsure or unable to get the information from the above, open an issue +in the [project issue tracker](https://github.com/jshttp/cookie/issues) +asking for the current contact information. + +To ensure the timely response to your report, please ensure that the entirety +of the report is contained within the email body and not solely behind a web +link or an attachment. + +At least one owner will acknowledge your email within 48 hours, and will send a +more detailed response within 48 hours indicating the next steps in handling +your report. After the initial reply to your report, the owners will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/index.js new file mode 100644 index 00000000..acd5acd6 --- /dev/null +++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/cookie/index.js @@ -0,0 +1,335 @@ +/*! + * cookie + * Copyright(c) 2012-2014 Roman Shtylman + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module exports. + * @public + */ + +exports.parse = parse; +exports.serialize = serialize; + +/** + * Module variables. + * @private + */ + +var __toString = Object.prototype.toString +var __hasOwnProperty = Object.prototype.hasOwnProperty + +/** + * RegExp to match cookie-name in RFC 6265 sec 4.1.1 + * This refers out to the obsoleted definition of token in RFC 2616 sec 2.2 + * which has been replaced by the token definition in RFC 7230 appendix B. + * + * cookie-name = token + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / + * "*" / "+" / "-" / "." / "^" / "_" / + * "`" / "|" / "~" / DIGIT / ALPHA + */ + +var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +/** + * RegExp to match cookie-value in RFC 6265 sec 4.1.1 + * + * cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + * ; US-ASCII characters excluding CTLs, + * ; whitespace DQUOTE, comma, semicolon, + * ; and backslash + */ + +var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/; + +/** + * RegExp to match domain-value in RFC 6265 sec 4.1.1 + * + * domain-value = + * ; defined in [RFC1034], Section 3.5, as + * ; enhanced by [RFC1123], Section 2.1 + * =